为什么GCC不能优化“x &&(x&4242)”到“x&4242”的逻辑按位AND对?
这里有两个function,我声称做同样的事情:
bool fast(int x) { return x & 4242; } bool slow(int x) { return x && (x & 4242); }
从逻辑上说,他们做同样的事情,只是为了100%肯定我写了一个testing,通过他们两个跑了所有40亿可能的投入,他们匹配。 但汇编代码是一个不同的故事:
fast: andl $4242, %edi setne %al ret slow: xorl %eax, %eax testl %edi, %edi je .L3 andl $4242, %edi setne %al .L3: rep ret
我感到惊讶的是,海合会无法跨越逻辑消除冗余testing。 我用-O2,-O3和-Os尝试了g ++ 4.4.3和4.7.2,所有这些都生成了相同的代码。 该平台是Linux x86_64。
有人可以解释为什么海湾合作委员会不应该足够聪明,在这两种情况下生成相同的代码? 我也想知道其他编译器是否可以做得更好。
编辑以添加testing装具:
#include <cstdlib> #include <vector> using namespace std; int main(int argc, char* argv[]) { // make vector filled with numbers starting from argv[1] int seed = atoi(argv[1]); vector<int> v(100000); for (int j = 0; j < 100000; ++j) v[j] = j + seed; // count how many times the function returns true int result = 0; for (int j = 0; j < 100000; ++j) for (int i : v) result += slow(i); // or fast(i), try both return result; }
我使用-O3在Mac OS上使用clang 5.1testing了以上版本。 使用fast()
花了2.9秒,使用slow()
3.8秒。 如果我使用全零vector,则两个函数之间的性能没有显着差异。
你是正确的,这似乎是一个缺陷,并可能是一个彻头彻尾的错误,在优化。
考虑:
bool slow(int x) { return x && (x & 4242); } bool slow2(int x) { return (x & 4242) && x; }
海合会发布的大会4.8.1(-O3):
slow: xorl %eax, %eax testl %edi, %edi je .L2 andl $4242, %edi setne %al .L2: rep ret slow2: andl $4242, %edi setne %al ret
换句话说, slow2
是错误的。
我只为GCC贡献了偶尔的补丁,所以无论我的观点是否承担任何重量都是值得商榷的:-)。 但在我看来,GCC优化其中一个而不是另一个是非常奇怪的。 我build议提交一个错误报告 。
[更新]
令人惊讶的小变化似乎有很大的不同。 例如:
bool slow3(int x) { int y = x & 4242; return y && x; }
…再次生成“慢”代码。 我对这种行为没有任何假设。
您可以在这里在多个编译器上进行所有这些实验。
究竟为什么它应该能够优化代码呢? 你假设任何有效的转换都将完成。 这根本不是优化器的工作原理。 他们不是人工智能。 他们只是通过参数化来replace已知的模式。 例如,“通用子expression式消除”扫描公共子expression式的expression式,并且如果不改变副作用,则向前移动它们。
(顺便说一句,CSE表明,优化器已经非常清楚在可能存在副作用的情况下允许哪些代码移动,他们知道你必须小心&&
expr && expr
是否可以CSE优化取决于expr
副作用)
那么总结一下:你认为哪种模式适用于这里?
这就是你的代码在ARM中看起来应该如何使input0时运行得更slow
。
fast(int): movw r3, #4242 and r3, r0, r3 adds r0, r3, #0 movne r0, #1 bx lr slow(int): cmp r0, #0 bxeq lr movw r3, #4242 and r3, r0, r3 adds r0, r3, #0 movne r0, #1 bx lr
但是,GCC在开始使用这些微不足道的function时会很好地进行优化。
bool foo() { return fast(4242) && slow(42); }
变
foo(): mov r0, #1 bx lr
我的观点是,有时候这样的代码需要更多的上下文来进一步优化,为什么优化器(改进者!)的实现者应该打扰?
另一个例子:
bool bar(int c) { if (fast(c)) return slow(c); }
变
bar(int): movw r3, #4242 and r3, r0, r3 cmp r3, #0 movne r0, #1 bxne lr bx lr
为了执行这个优化,需要研究两个不同情况的expression式: x == 0
,简化为false
, x != 0
,简化为x & 4242
。 然后足够聪明地看到,即使对于x == 0
,第二个expression式的值也会产生正确的值。
让我们想象一下,编译器执行一个案例研究并发现简化。
如果x != 0
,expression式简化为x & 4242
。
如果x == 0
,expression式简化为false
。
简化后,我们得到两个完全不相关的expression式。 为了调和它们,编译器应该问非自然的问题:
如果x != 0
,可以使用false
来代替x & 4242
吗? [没有]
如果x == 0
,那么x & 4242
可以用来代替false
吗? [是]
我最后编译的编译器没有做这种优化。 编写一个优化器来利用二进制和逻辑运算符相关的优化,不会加速应用程序的运行速度。 主要的原因是人们不经常使用二进制运算符。 许多人不喜欢二元操作符,那些通常不会写无用的操作,需要优化。
如果我去写作的麻烦
return (x & 4242)
我明白这是什么意思,为什么我会打扰额外的一步。 出于同样的原因,我不会写这个不理想的代码
if (x==0) return false; if (x==1) return true; if (x==0xFFFEFD6) return false; if (x==4242) return true; return (x & 4242)
编译器开发的时间比使用没有区别的东西更好。 编译器优化中只有这么多大鱼才会炒。
注意到这个优化在所有机器上都是无效的。 特别是如果你运行在使用负数的补码表示的机器上,那么:
-0 & 4242 == true -0 && ( -0 & 4242 ) == false
海湾合作委员会从来没有支持这样的表示,但它们是由C标准允许的。
C对有符号整型和非无符号整型的行为限制较less。 使用位操作时,负值可以合法地做出奇怪的事情。 如果任何可能的位操作参数都具有合法的不受约束的行为,编译器将不能删除它们。
例如,如果除以零,“x / y == 1或者true”可能会使程序崩溃,所以编译器不能忽略除法的评估。 负数的签名值和位操作从来没有在任何普通系统上做这样的事情,但是我不确定语言定义是否规定了它。
你应该尝试用unsigned int的代码,看看是否有帮助。 如果是这样,你会知道这是一个types问题,而不是expression式。