问题说明了一切。有谁知道以下......
size_t div(size_t value) {
const size_t x = 64;
return value / x;
}
...优化为?
size_t div(size_t value) {
return value >> 6;
}
编译器会这样做吗? (我的兴趣在于GCC)。有没有这种情况,有些情况没有吗?
我真的很想知道,因为每当我写一个可以像这样进行优化的师时,我会花费一些心理能量来思考是否会浪费一秒钟的宝贵事情来做一个转变就足够的师。
答案 0 :(得分:45)
即使使用g++ -O0
(是的,-O0
!),也会发生这种情况。您的函数编译为:
_Z3divm:
.LFB952:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
movq %rdi, -24(%rbp)
movq $64, -8(%rbp)
movq -24(%rbp), %rax
shrq $6, %rax
leave
ret
注意shrq $6
,这是一个右移6位。
使用-O1
,删除不必要的垃圾:
_Z3divm:
.LFB1023:
movq %rdi, %rax
shrq $6, %rax
ret
g ++ 4.3.3,x64的结果。
答案 1 :(得分:29)
大多数编译器甚至比通过将2的幂除以换算更进一步 - 它们通常将整数除以一个常数转换为一系列乘法,移位和加法指令以获得结果而不是使用CPU的内置-in divide指令(如果有的话)。
例如,MSVC将除以71转换为以下内容:
// volatile int y = x / 71;
8b 0c 24 mov ecx, DWORD PTR _x$[esp+8] ; load x into ecx
b8 49 b4 c2 e6 mov eax, -423447479 ; magic happens starting here...
f7 e9 imul ecx ; edx:eax = x * 0xe6c2b449
03 d1 add edx, ecx ; edx = x + edx
c1 fa 06 sar edx, 6 ; edx >>= 6 (with sign fill)
8b c2 mov eax, edx ; eax = edx
c1 e8 1f shr eax, 31 ; eax >>= 31 (no sign fill)
03 c2 add eax, edx ; eax += edx
89 04 24 mov DWORD PTR _y$[esp+8], eax
所以,你得到一个除以71的乘法,几次移位和一对加法。
有关正在发生的事情的详细信息,请参阅Henry Warren的“Hacker's Delight”一书或配套网页:
有online added chapter提供了一些关于常数除法的附加信息,使用乘法/ shift / add和幻数,以及page with a little JavaScript program来计算你需要的幻数。
本书的配套网站非常值得一读(就像本书一样) - 特别是如果你对比特级微优化感兴趣的话。
我刚才发现的另一篇讨论此优化的文章:http://blogs.msdn.com/devdev/archive/2005/12/12/502980.aspx
答案 2 :(得分:19)
只有当它可以确定参数是正数时。对于你的例子来说就是这种情况,但是自从C99为整数除法指定了向零舍入语义之后,将两次幂的除法优化变得更加困难,因为它们给出了负面参数的不同结果。
对迈克尔下面的评论作出回应,这里有一种方法r=x/p;
x
p
除以if (x<0)
x += p-1;
r = x >> (log2 p);
已知的幂{{1}}确实可以由编译器翻译:
{{1}}
由于OP在询问他是否应该考虑这些事情,一个可能的答案是“只有你知道股息的符号比编译器更好或者知道如果结果向0舍入并且无关紧要 - 或者 - ∞”。
答案 3 :(得分:3)
是的,编译器为这种简单的计算生成最优的代码。但是,为什么你坚持要求“轮班”,我不清楚。给定平台的最佳代码可能很容易变成与“转变”不同的东西。
在一般情况下,“转变”在某种程度上是实现二次幂乘法和除法的最佳方式的旧的和被挨打的想法在现代平台上几乎没有实际意义。这是向新手说明“优化”概念的一种好方法,但仅此而已。
您的原始示例并不具有代表性,因为它使用无符号类型,这极大地简化了除法运算的实现。如果操作数被签名,那么C和C ++语言的“舍入为零”要求就不可能只进行转换。