为什么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 ,简化为falsex != 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式。