如何整数除以负数*向下*?
Posted
技术标签:
【中文标题】如何整数除以负数*向下*?【英文标题】:How to integer-divide round negative numbers *down*? 【发布时间】:2010-06-15 00:58:10 【问题描述】:似乎每当我用一个负整数除以一个正整数时,我都需要它向下舍入 (向 -inf),而不是向 0。但 C# 和 C++ 都向 0 舍入。
所以我想我需要一个 DivideDownward() 方法。我可以用几行来写它,测试是否定的等等,但我的想法似乎很糟糕。所以我想知道我是否遗漏了什么,以及你是否有一种“优雅”的方式来向下舍入负除法。
【问题讨论】:
举例说明你想要的行为。 例子?好的,我需要 (-5)/3 来产生 -2,而不是 -1。但我真的不认为这个例子对(IMO)明确的问题有任何补充。 【参考方案1】:每当我将一个负整数除以一个正整数时,我都需要将它向下舍入。
这是地狱,不是吗? Knuth 写了为什么这是做事的正确方法,但我们被传统的整数硬件所困。
如果您能承受精度损失,最简单和最干净的方法是将 32 位整数转换为 64 位 double
并使用 FP 舍入模式向负无穷大舍入当您将商转换回整数时。今天的浮点单元非常快,实际上可能比整数单元除法快;可以肯定的是,你必须测量。
如果您需要完整的 64 位整数精度,作为编译器编写者,我已经通过执行两个条件分支来处理这个问题,这样您就可以对幅度进行划分,然后得到正确的符号。但这是不久前条件分支与除法相比便宜的时候了。在今天的硬件上,我必须先进行试验,然后才能推荐一些东西。
原则上,您可以使用传统的英特尔 80 位浮点数在 64 位整数上提取浮点技巧,但它非常不可移植,我不相信英特尔会继续制作那个单位快。这些天浮点速度在 SSE 单元中。
寻找其他技巧的地方包括 Hank Warren 的书 Hacker's Delight(我的副本正在工作)和标准 ML 的 MLton 编译器,它需要整数除法以向负无穷舍入。
无论你做什么,当你确定它时,如果你使用的是 C++ 或 C99,请将你的除法例程粘贴到一个 .h 文件中并使其成为static inline
。这样一来,当您的解决方案对于 5 年内交付的新 whizbang 硬件来说并不理想时,您可以在一个地方进行更改。
【讨论】:
我认为 int32 -> double 不会失去任何精度。但是我刚刚优化了一些 fp 代码,其中 fstp 操作(接近 0 的数字)占用了我大部分的 CPU 时间。也许不相关,但我现在对不必要的 fp 感到厌烦。 ;)【参考方案2】:假设 b 始终为正,这是一个便宜的解决方案:
inline int roundDownDivide(int a, int b)
if (a >= 0) return a/b;
else return (a-b+1)/b;
【讨论】:
roundDownDivide(-2147483648, 2) == 1073741823
-- 如果a < INT_MIN + b - 1
会溢出,同时不是最快的。详细评估见我的回答。【参考方案3】:
您可以通过这样做摆脱任何分支:
inline int DivideRoundDown(int a_numerator, int a_denominator)
return (a_numerator / a_denominator) + ((a_numerator % a_denominator) >> 31);
【讨论】:
这个函数是否有任何假设,或者它是否适用于正/负操作数的所有组合?【参考方案4】:如果您想以相对简洁的方式仅使用整数来编写此代码,则可以这样编写:
var res = a / b - (a % b < 0 ? 1 : 0);
这可能编译成相当多的指令,但它可能仍然比使用浮点更快。
【讨论】:
OP 没有提到 C,但仍然值得注意的是,由于实现定义的百分比,这不能保证在 C89 中工作。我认为您可以通过更改为a % b != 0
来概括它。
@Matthew:这很棘手。但是,如果 a % b
返回一个大于零的数字,则它需要返回 0(这就是为什么我在 C# 中需要 <
)。
@Tom,说得好。我在想a
是否定的。所以我猜一个通用版本是:a /b - (a < 0 && a % b != 0 ? 1 : 0)
我认为给定的答案取决于实现,但 Matthew 的修改显得简洁可靠。如果这是一个答案,我会选中它。谢谢(大家)!
@Matthew:实际上,这保证 C89 和 C99 都可以工作——他们要求 (a/b)*b + a%b == a
除非 b == 0
,即使 /
和 %
返回的实际值是为负操作数定义的实现。如果内置 / 向 -inf 而不是 0 舍入,您的版本将失败。【参考方案5】:
警告:对于 a=-1 的输入,这篇文章会产生不正确的结果。请查看其他答案。 -摄像头
[c++]
这可能是您所说的“kludgey”的意思,但这是我想出的;)
int divideDown(int a, int b)
int r=a/b;
if (r<0 && r*b!=a)
return r-1;
return r;
在 if 语句中,我输入了 r
if (a<0 && b>0)
这与您的描述一致“似乎每当我将 负整数除以正整数时”。
【讨论】:
这是我所说的klugey,但我不知道有什么更好的。特别是对于 r*b!=a 测试,它可能比 a%b!=0 更有效,并且与提供的单行表达式相比,证明了额外的代码行是合理的,我授予这个“答案”。 这个我不确定,但不应该是if (r<=0 && r*b!=a)
吗?
比这更糟糕——正如所写的那样,这会产生 a=-1 的错误答案,如果将其更改为 r
无法删除,因为它是公认的答案。将尝试使用解决方案进行修复,但在此之前已编辑并带有警告。感谢您发现错误!
@Cam 我认为即使它已经被接受,也可以删除您自己的答案。事实上,我认为这样做有某种自我批评的标志。也许不是。【参考方案6】:
许多编程语言,包括 C 和 C++ 的旧标准,都保证了除法规则,即
a = b * (a / b) + a % b
即使 a/b
和 a%b
的确切值未定义。[0] 这可以被利用来计算在许多语言和平台中使用(等价于)的所需结果以下代码:
int divF(int a, int b) return a / b - (a % b < 0);
这是来自@TomasPetricek 答案的版本。但是,它仅适用于b > 0
!
以下代码适用于任何b != 0
:[1]
int sign(int x) return (x > 0) - (x < 0);
int divF2(int a, int b) return a / b - (sign(a % b) == -sign(b));
但是,四舍五入的除法(又名地板除法,又名Knuth 的除法)并不总是可取的。有人认为[2] 欧几里得除法是最常用的除法。 b > 0
向下取整,b < 0
向上取整。它具有很好的属性,即对于所有a
和b
,兼容定义的余数的值始终为非负,与它们的符号无关。此外,它与二进制补码机器上的位移和掩码相一致,用于二次除数的幂。是的,计算起来也更快:
int divE(int a, int b)
int c = a % b < 0;
return a / b + (b < 0 ? c : -c);
所有三个版本都在 amd64 上使用 clang -O3
生成无分支代码。但是,在二进制补码架构上,以下可能会稍微快一些:
int divE2(int a, int b) return a / b + (-(a % b < 0) & (b < 0 ? 1 : -1));
生成的代码
divF:
cdq
idiv esi
sar edx, 31
add eax, edx
divF2:
cdq
idiv esi
xor ecx, ecx
test edx, edx
setg cl
sar edx, 31
add edx, ecx
xor ecx, ecx
test esi, esi
setg cl
shr esi, 31
sub esi, ecx
xor ecx, ecx
cmp edx, esi
sete cl
sub eax, ecx
Chazz:
cdq
idiv esi
test edx, edx
cmove edi, esi
xor edi, esi
sar edi, 31
add eax, edi
divE:
cdq
idiv esi
mov ecx, edx
shr ecx, 31
sar edx, 31
test esi, esi
cmovs edx, ecx
add eax, edx
divE2:
cdq
idiv esi
sar edx, 31
shr esi, 31
lea ecx, [rsi + rsi]
add ecx, -1
and ecx, edx
add eax, ecx
基准
simple truncating division:
2464805950: 1.90 ns -- base
euclidean division:
2464831322: 2.13 ns -- divE
2464831322: 2.13 ns -- divE2
round to -inf for all b:
1965111352: 2.58 ns -- Chazz
1965111352: 2.64 ns -- divF2
1965111352: 5.02 ns -- Warty
round to -inf for b > 0, broken for b < 0:
1965143330: 2.13 ns -- ben135
1965143330: 2.13 ns -- divF
1965143330: 2.13 ns -- Tomas
4112595000: 5.79 ns -- runevision
round to -inf, broken for b < 0 or some edge-cases:
4112315315: 2.24 ns -- DrAltan
1965115133: 2.45 ns -- Cam
1965111351: 7.76 ns -- LegsDrivenCat
在 FreeBSD 12.2 上使用 clang -O3
编译,i7-8700K CPU @ 3.70GHz。第一列是产生相同结果的校验和分组算法。 base
是用于衡量测试开销的最简单的截断除法。
试验台:
static const int N = 1000000000;
int rng[N][2], nrng;
void push(int a, int b) rng[nrng][0] = a, rng[nrng][1] = b, ++nrng;
SN_NOINLINE void test_case(int (*func)(), const char *name)
struct timespec t0, t1;
clock_gettime(CLOCK_PROF, &t0);
int sum = func();
clock_gettime(CLOCK_PROF, &t1);
double elapsed = (t1.tv_sec - t0.tv_sec)*1.e9 + (t1.tv_nsec - t0.tv_nsec);
printf("%10u: %5.2f ns -- %s\n", sum, elapsed/N, name);
#define ALL_TESTS(X) X(base) X(divF) X(divF2) X(divE) X(divE2) X(ben135) X(DrAltan) X(Cam) X(Tomas) X(Warty) X(Chazz) X(runevision) X(LegsDrivenCat)
#define LOOP_TEST(X) \
SN_NOINLINE int loop_##X() \
int sum = 0; \
for(int i = 0; i < N; ++i) sum += X(rng[i][0], rng[i][1]); \
return sum; \
\
/**/
ALL_TESTS(LOOP_TEST)
int main()
srandom(6283185);
push(INT_MAX, 1); push(INT_MAX, -1); push(INT_MIN, 1);
push(INT_MAX, 2); push(INT_MAX, -2); push(INT_MIN, 2); push(INT_MIN, -2);
while(nrng < N)
int a = random() - 0x40000000, b;
do b = (random() >> 16) - 0x4000; while(b == 0);
push(a,b);
#define CALL_TEST(X) test_case(loop_##X, #X);
ALL_TESTS(CALL_TEST)
脚注/参考文献
-
如今,它们在 C 和 C++ 中定义为通过截断舍入。所以负数向上舍入,正数向下舍入。这就是硬件除数所做的。这很糟糕,因为它是最没用的四舍五入规则。
Daan Leijen. Division and Modulus for Computer Scientists. December 2001.
Raymond T. Boute. The Euclidean definition of the functions div and mod. April 1992.
【讨论】:
【参考方案7】:Math.Floor((double)a/(double)b)
如果你需要它作为一个 int,之后再转换它(int)Math.Floor((double)a/(double)b)
【讨论】:
decimal
是slowest 数字类型,它不会在此处添加任何内容。 float
或 double
会更合适。
你也不需要投分母,只需要投分子。
@Matthew Flaschen:我正在考虑这个问题。我没有意识到他所说的 int 是 int32。如果插入 a 和 b [Int64.MaxValue-1 和 Int64.MaxValue 分别] 的无关值,使用 double 会因浮点错误而中断,您将得到 1 而不是 0。答案已修复。
这个答案有效,但考虑到 fp 的性能成本,我认为它对我的“kludgey”方法想法没有任何改进。【参考方案8】:
对于 2 的幂,取决于您的编译器实现 (Visual Studio),您可以使用右移。
-5 >> 2 == -2
【讨论】:
【参考方案9】:这是一个在除数为负数时有效的版本:
int floor_div2(int a, int b)
int rem = a % b;
int div = a/b;
if (!rem)
a = b;
int sub = a ^ b;
sub >>= 31;
return div+sub;
【讨论】:
这是此页面上最快的解决方案,基于我的基准测试,它为所有边缘情况提供了正确的结果。来自我的巨大 +1。【参考方案10】:上面的 Atlans 博士解决方案是最普遍的答案,但如果您知道 a 不会小于某个固定数字,您可以添加 b 的最小倍数,使其成为正数,然后调整结果。例如,如果 a 始终 >= -10 * b,您可以说:
return (a + 10 * b) / b - 10
或者如果您只想要 a 为正数或 0 时的正确结果,而当 a 为负数时 some 为负数(例如,用于测试结果是否 >= 0 而不包括范围 a = [ -1 ; -b +1]) 你可以这样做
return (a + b) / b - 1;
这将为 [-1 , -b + 1] 以及 [-b ] 返回 -1 ; -2 * b + 1],但如果你只是想确定除法的结果是否为正,这并不重要。实际上,我们只是更改了舍入,使其向 -1 而不是 0 舍入。
【讨论】:
【参考方案11】:我不是低级优化方面的专家,但这里有一个没有分支(条件)的版本,它也不需要转换为浮点数。在 C# 中测试,它似乎比使用分支的版本快得多。
return (a - (((a % b) + b) % b)) / b;
这个解决方案部分来源于关于如何为负数做最佳模函数的讨论:Modulo of negative numbers
【讨论】:
【参考方案12】:编辑:我的解决方案并非在所有情况下都有效。这是Dr.Atlan's解决方案的C#实现:
/// <summary>
/// Divides a/b, rounding negative numbers towards -Inf.
/// </summary>
/// <param name="a">Dividend</param>
/// <param name="b">Divisor; must be positive for correct results</param>
/// <returns>a ÷ b</returns>
public static int Div(int a, int b)
return a < 0 ? (a - b + 1) / b : a / b;
【讨论】:
不适用于可以无余数的数字。例如,Div(-1, 1)
返回 -2。
@BrankoDimitrijevic:哈!这解释了我游戏中的伪影。现在改用 Dr.Altan 的解决方案;那个似乎运作良好。【参考方案13】:
通过偏移 2^31 以 unsigned long 进行计算,例如:
int floor_div(int a, int b)
unsigned long aa = a + (1UL << 31);
unsigned long cc = (aa / b) - (1UL << 31);
return (int)cc
我还没有测试过。
【讨论】:
floor_div(6, 2) == -1431655764
? O.o 这家伙应该获得“此页面上最糟糕的解决方案”的徽章。【参考方案14】:
我是这样做的(只有一个除法,没有乘法,没有模,看起来是最快的解决方案):
(a < 0 && b > 0) ? (a - b + 1) / b : (a > 0 && b < 0) ? (a - b - 1) / b : a / b
我很好奇什么更快:更少的分支或更少的乘法/除法/模数,所以我将我的解决方案与 runevision 的解决方案进行了比较,在这种特殊情况下,更少的除法比更少的分支更好。 ben135 的解决方案在某些情况下会给出不正确的结果(例如 10/-3 给出 -3 但应该是 -4)。
【讨论】:
以上是关于如何整数除以负数*向下*?的主要内容,如果未能解决你的问题,请参考以下文章