C++ 为什么可以';t GCC优化“中的逻辑位和对”;x&&;(x"4242)";至;x&;4242“吗;?
这里有两个函数,我声称它们的作用完全相同:C++ 为什么可以';t GCC优化“中的逻辑位和对”;x&&;(x"4242)";至;x&;4242“吗;?,c++,gcc,optimization,compiler-optimization,C++,Gcc,Optimization,Compiler Optimization,这里有两个函数,我声称它们的作用完全相同: bool fast(int x) { return x & 4242; } bool slow(int x) { return x && (x & 4242); } 从逻辑上讲,它们做的是相同的事情,为了100%确定,我写了一个测试,通过它们运行了所有40亿个可能的输入,并且它们匹配。但汇编代码是另一回事: fast: andl $4242, %edi setne %al r
bool fast(int x)
{
return x & 4242;
}
bool slow(int x)
{
return x && (x & 4242);
}
从逻辑上讲,它们做的是相同的事情,为了100%确定,我写了一个测试,通过它们运行了所有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
我感到惊讶的是,GCC无法跨越逻辑来消除冗余测试。我用-O2、-O3和-Os尝试了g++4.4.3和4.7.2,它们都生成了相同的代码。该平台是Linux x86_64
有人能解释为什么GCC不应该聪明到在这两种情况下生成相同的代码吗?我还想知道其他编译器是否能做得更好
编辑以添加测试线束:
#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;
}
#包括
#包括
使用名称空间std;
int main(int argc,char*argv[])
{
//使用从argv[1]开始的数字填充向量
int seed=atoi(argv[1]);
向量v(100000);
对于(int j=0;j<100000;++j)
v[j]=j+种子;
//计算函数返回true的次数
int结果=0;
对于(int j=0;j<100000;++j)
对于(int i:v)
结果+=慢(i);//或快(i),请两者都尝试
返回结果;
}
我在带有-O3的Mac操作系统上用Clang5.1测试了上述内容。使用fast()
需要2.9秒,使用slow()
需要3.8秒。如果我改为使用全零向量,那么这两个函数的性能没有显著差异。到底为什么它能够优化代码?您假设任何有效的转换都将完成。这根本不是优化器的工作方式。它们不是人工智能。它们只是通过参数化替换已知模式来工作。例如,“公共子表达式消除”会扫描表达式中的公共子表达式,如果这不会改变副作用,则会将其向前移动
(顺便说一句,CSE表明优化器已经非常清楚在可能存在副作用的情况下允许哪些代码移动。他们知道您必须小心&&
。是否可以对expr&&expr
进行CSE优化取决于expr
的副作用)
总之,您认为哪种模式适用于这里?这是在ARM中,当输入0时,它应该使变慢
运行得更快
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都会很好地进行优化
bool foo() {
return fast(4242) && slow(42);
}
变成
foo():
mov r0, #1
bx lr
bar(int):
movw r3, #4242
and r3, r0, r3
cmp r3, #0
movne r0, #1
bxne lr
bx lr
我的观点是,有时候这样的代码需要进一步优化更多的上下文,那么为什么优化器的实现者(改进者!)会费心呢
另一个例子:
bool bar(int c) {
if (fast(c))
return slow(c);
}
bool slow3(int x)
{
int y = x & 4242;
return y && x;
}
变成
foo():
mov r0, #1
bx lr
bar(int):
movw r3, #4242
and r3, r0, r3
cmp r3, #0
movne r0, #1
bxne lr
bx lr
要执行此优化,需要研究两种不同情况下的表达式:
x==0
,简化为false
,以及x!=0,简化为x&4242
。然后要足够聪明地看到,即使对于x==0
,第二个表达式的值也会产生正确的值
让我们假设编译器执行一个案例研究并找到简化
如果x!=0时,表达式简化为x&4242
如果x==0
,表达式将简化为false
经过简化,我们得到了两个完全无关的表达式。要协调这些问题,编译器应提出非自然的问题:
如果x!=0
,是否可以使用false
代替x&4242
?[否]
如果x==0
,是否可以使用x&4242
代替false
?[是]您是正确的,这似乎是优化器中的一个缺陷,可能是一个彻底的错误
考虑:
bool slow(int x)
{
return x && (x & 4242);
}
bool slow2(int x)
{
return (x & 4242) && x;
}
GCC 4.8.1(-O3)排放的组件:
换句话说,slow2
的名称有误
我只是偶尔向GCC提供了一个补丁,所以我的观点是否有分量值得商榷:-)。但在我看来,GCC优化其中一个而不是另一个确实很奇怪。我建议
[更新]
令人惊讶的是,微小的变化似乎产生了巨大的变化。例如:
bool bar(int c) {
if (fast(c))
return slow(c);
}
bool slow3(int x)
{
int y = x & 4242;
return y && x;
}
…再次生成“慢”代码。我对这种行为没有任何假设
您可以在多个编译器上试验所有这些特性。C对有符号整数类型的行为的限制比无符号整数类型少。负值尤其可以合法地对位操作进行奇怪的操作。如果位运算的任何可能参数具有合法的无约束行为,编译器将无法删除它们
例如,“x/y==1或true”如果被零除可能会使程序崩溃,因此编译器不能忽略除法的计算。负符号值和位操作在任何公共系统上都不会执行类似的操作,但我不确定语言定义是否排除了这一点
您应该尝试使用无符号整数的代码,看看这是否有帮助。如果是这样,您就会知道这是类型问题,而不是表达式问题。值得注意的是,这种优化并不是在所有机器上都有效。特别是,如果您运行的机器使用负数的补码表示,则:
-0 & 4242 == true
-0 && ( -0 & 4242 ) == false
GCC从未支持这种表示,但C标准允许它们。我上一次使用的编译器没有进行这种优化。编写优化器以利用与组合二进制运算符和逻辑运算符相关的优化不会加快应用程序的速度。主要原因是人们不经常使用这样的二进制运算符。许多人对二进制运算符感到不舒服,而那些使用二进制运算符的人通常不会编写需要优化的无用操作
如果我费心写作
return (x & 4242)
我明白这意味着什么为什么我要