以下哪种技术是将整数除以2的最佳选项,为什么?
技术1:
x = x >> 1;
技巧2:
x = x / 2;
这里的x
是一个整数。
以下哪种技术是将整数除以2的最佳选项,为什么?
技术1:
x = x >> 1;
技巧2:
x = x / 2;
这里的x
是一个整数。
选用最符合你所需的操作。
请注意它们并不完全等价。对于负整数,它们可能会产生不同的结果。例如:
-5 / 2 = -2
-5 >> 1 = -3
%
和/
运算符必须对于正数和负数操作数保持一致,以便(a/b)*b+(a%b)==a
无论a
和b
的符号如何都成立。通常,作者会做出最优性能的选择,以充分发挥CPU的性能。 - RBerteig第一个看起来像除法吗?不是的。如果你想要除法,使用x / 2
。编译器可以在可能的情况下优化它为位移操作(这被称为强度降低),如果你自己这样做会变成无用的微小优化。
x/2
。https://godbolt.org/z/G9TTYG6Ke 因此,对于有符号数来说,x>>1
更便宜,但是如果编译器不太合理,x/2
将不会使用idiv
。这个答案可以更好地解释如果不能使用单个shr
或sar
时会发生什么。 - Peter Cordes进一步补充:有很多理由支持使用x = x / 2;
,以下是其中的一些:
它更清晰地表达了你的意图(假设你不处理位操作寄存器位或其他类似情况)
编译器会将其优化为移位操作
即使编译器没有将其优化为移位操作且选择了一个比移位更慢的操作,它对程序性能的影响也微乎其微,几乎可以忽略不计(如果确实会明显影响性能,那么你就有一个实际理由使用移位操作)
如果除法运算是一个更大表达式的一部分,如果你使用除法运算符,则更可能正确设置优先级:
x = x / 2 + 5;
x = x >> 1 + 5; // not the same as above
有符号算术可能会比上面提到的优先级问题更加复杂。
再次强调 - 编译器实际上已经为您执行此操作。实际上,它将除以常数的运算转换为一系列移位、加法和乘法,适用于各种数字,而不仅仅是2的幂。请参见此问题,其中包含更多相关信息的链接。
简而言之,使用移位运算代替乘法或除法并不能带来任何好处,反而可能增加引入错误的可能性。编译器可以很聪明地将这种操作优化为移位运算,这样已经有很长时间了。
a/b/c*d
(其中a..d
代表数字变量)而不是更可读的 (a*d)/(b*c)
的表达式。 - user554546a*d
或b*c
可能会产生溢出,那么不太易读的形式就不等同于原来的形式,并且具有明显的优势。顺便说一下,我同意括号是你最好的朋友。 - Mark Ransomx
永远不会是奇数负数或者你不关心off-by-one错误,那么代码x=x/2;
比x>>=1
更加"清晰"。否则,x=x/2;
和x>>=1;
有不同的含义。如果需要的是由x>>=1
计算出的值,我认为这比使用除以二的任何其他公式如x = (x & ~1)/2
或x = (x < 0) ? (x-1)/2 : x/2
更清晰。同样,如果需要由x/=2
计算出的值,则比((x + ((unsigned)x>>31)>>1)
更清晰。 - supercat只需使用除法运算符 (/
),假定这更加清晰易懂。编译器会根据情况进行优化。
ASSUME(x >= 0); x /= 2;
而不是 x >>= 1;
,但这仍然是一个重要的观点需要提出来。 - David Stone我同意其他答案的观点,您应该倾向于使用x / 2
,因为它的意图更清晰,编译器应该会为您进行优化。
然而,喜欢x / 2
而不是x >> 1
的另一个原因是,如果x
是有符号的int
并且是负数,则>>
的行为取决于实现。
来自ISO C99标准的第6.5.7节,第5个小点:
E1 >> E2
的结果是E1
向右移动E2
位。 如果E1
具有无符号类型或者E1
具有带符号类型和非负值, 则结果的值是E1
/2E2
的商的整数部分。 如果E1
具有带符号类型和负值,则结果的值是 实现定义的。
x>>scalepower
定义的行为恰好适用于将值除以二的幂次方,例如用于屏幕渲染等目的,而使用 x/scalefactor
将是错误的,除非对负值进行修正。 - supercat>>
定义为算术右移,即对于二进制补码,移位时会复制符号位。例如,GNU C 保证了这一点:https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html。当然,Deathstation 9000 可能会执行一些无用的操作,但这对大多数代码来说并不是一个重要的可移植性问题。ISO C++ 甚至正在朝着将其作为算术右移进行标准化的方向发展。 - Peter Cordesx / 2
更清晰,而x >> 1
并没有明显加速(根据微基准测试,Java JVM大约快了30%)。正如其他人所指出的那样,对于负数,四舍五入略有不同,因此在处理负数时需要考虑这一点。一些编译器可能会自动将x / 2
转换为x >> 1
,如果它们知道数字不能是负数(即使我无法验证这一点)。
x / 2
也可能不使用(慢速的)除法 CPU 指令,因为 可能存在某些快捷方式,但它仍然比 x >> 1
慢。x >>> 1
,它又与众不同。它允许正确计算两个值的平均值,因此即使对于非常大的 a
和 b
值,(a + b) >>> 1
将返回平均值。例如,如果数组索引可能非常大,则需要这样做才能进行二分查找。在许多版本的二分查找中存在一个错误,因为它们使用 (a + b) / 2
来计算平均值。这并不能正常工作。正确的解决方案是使用 (a + b) >>> 1
。)x/2
转换为 x>>1
,特别是当 x
可能为负数时。如果想要的是 x>>1
计算出的值,那么几乎肯定比任何涉及计算相同值的 x/2
表达式更快。 - supercatx/2
转换为 x>>1
。我会尝试更新我的答案。 - Thomas Muellerdiv
指令,而是将x/2
转换为(x + (x<0?1:0)) >> 1
(其中>>是算术右移,它将符号位移入)。这需要4条指令:复制值,shr(以在寄存器中获取符号位),加法,sar。http://goo.gl/4F8Ms4 - Peter CordesKnuth说:
过早优化是万恶之源。
因此,我建议使用x /= 2;
这样代码易于理解,而且我认为在该形式下对此操作进行优化对处理器来说不会有太大的差异。
int div2signed(int a) {
return a / 2;
}
并且这将编译为
movl %edi, %eax
shrl $31, %eax # (unsigned)x >> 31
addl %edi, %eax # tmp = x + (x<0)
sarl %eax # (x + 0 or 1) >> 1 arithmetic right shift
ret
int shr2signed(int a) {
return a >> 1;
}
输出结果为:
sarl %edi
movl %edi, %eax
ret
其他指令集同样可以高效地执行此操作,甚至可能更高效。例如,AArch64架构的GCC使用:
add w0, w0, w0, lsr 31 // x += (unsigned)x>>31
asr w0, w0, 1 // x >>= 1
ret
d
的区域。这种分区对许多目的都很有用。即使你想要断点不在 0 和 -1 之间,添加一个偏移量就可以移动它。满足第二公理的整数除法将把数线分成大部分大小为 d
的区域,但其中一个区域的大小为 2*d-1
。并非完全“平等”的划分。你能提供一些奇怪的划分实际上有用的建议吗? - supercatsar
)来实现有符号整数的>>操作。这篇邮件列表帖子(https://gcc.gnu.org/ml/gcc/2000-04/msg00152.html)证实了gcc历史上使用算术移位来处理有符号整数,因此FreeBSD gcc 4.2.1使用无符号移位的可能性非常小。我更新了你的帖子以修复这个问题,并且修改了早期段落中说两者都使用shr的错误描述,实际上它们都使用SAR。SHR是用于提取“/”情况下的符号位的。同时,我还包含了一个godbolt链接。 - Peter Cordes额外说明:
x *= 0.5 在一些基于虚拟机的语言中——特别是 ActionScript 中,往往比直接除以2更快,因为变量不必检查除以0的情况。
eval
)每次都会重新发生这种情况。无论如何,是的,这是一个非常糟糕的测试,因为它是一个非常愚蠢的优化。 - Ry-
x
,那么这两种方式都不太合适:应该是x >>= 1
或者x /= 2
,具体取决于你希望表达的操作意义。这并不是因为它更快(现代编译器会将所有等效变体编译为相同、快速的汇编代码),而是因为它更清晰易懂。 - leftaroundaboutx = x >>> 1
。另请注意,根据平台和编译器的不同,手动优化除法和乘法使用移位操作可能是相当合理的。举个例子,微控制器没有直接的ALU支持乘法。 - JimmyBx /= 2
,因为x >>= 1
看起来太像单子绑定(monadic bind)了 ;) - fredoverflowx = x / 2
比写x /= 2
更易读。这可能是主观偏好 :) - JimmyB