考虑以下C程序:
void bar();
void baz();
void foo( int a ) {
if ( a ) {
bar();
}
else {
baz();
}
}
在我的基于x86-64的计算机上,GCC使用-O1优化级别生成的指令给出:
0: sub $0x8,%rsp
4: test %edi,%edi
6: je 14 <foo+0x14>
8: mov $0x0,%eax
d: callq 12 <foo+0x12> # relocation to bar
12: jmp 1e <foo+0x1e>
14: mov $0x0,%eax
19: callq 1e <foo+0x1e> # relocation to baz
1e: add $0x8,%rsp
22: retq
而添加-freorder-blocks优化参数(包含在-O2中)会将代码转换为:
0: sub $0x8,%rsp
4: test %edi,%edi
6: jne 17 <foo+0x17>
8: mov $0x0,%eax
d: callq 12 <foo+0x12> # relocation to baz
12: add $0x8,%rsp
16: retq
17: mov $0x0,%eax
1c: callq 21 <foo+0x21> # relocation to bar
21: add $0x8,%rsp
25: retq
主要是从跳跃等于到跳跃不等于的变化。我知道,直到奔腾4,条件前向分支上的静态分支预测被认为是处理器没有采用(似乎静态预测在其他英特尔处理器上变得随机),因此我想这个优化正在解决这个问题。
假设并且引用 jne 优化版本,这意味着 else 块实际上被认为比 更可能执行
但这究竟是什么意思?由于编译器对 foo 函数中的 a 值没有假设,因此这种可能性仅取决于程序员的文字(事实上谁可以使用{{1}而不是if ( !a )
和反向函数调用)。
这是否意味着将 if 条件块视为例外情况(而不是正常的执行流程)应该被视为一种好的做法?
那是:
if ( a )
而不是:
if ( !cond ) {
// exceptional code
}
else {
// normal continuation
}
(当然,人们可能更喜欢在相关块中使用return语句来限制缩进大小。)
答案 0 :(得分:4)
我曾经在ARM(7,9)上进行过大量的性能优化操作。这是简单的C,愚蠢的编译器(SDT AFAIR)。保存一些CPU资源的方法之一是分析if
分支并重写if
条件,因此正常流不会破坏线性指令序列。这具有积极的影响,因为CPU预测阻止了更有效的使用,并且更有效的代码段内存缓存使用。
我认为在这里我们看到非常接近的优化。在第一个代码片段中,两个分支都导致正常序列被破坏(一个分支的行为6
,另一个分行为12
)。在第二个片段中,一个分支指令被排序到retq
,而其他分支序列具有单个跳转(不比第一个片段中的更差)。请注意2 retq
条说明。
因为我可以看到这不是je
或jne
的问题,而是块重新排序的问题所以分支是线性指令序列,其中一个输入没有任何jump
和完全预测块功率已保存。
关于“为什么GCC更喜欢一个分支而不是另一个分支”......我在文档中看到这可能是静态分支预测的结果(基于翻译单元内的调用?)。无论如何,我建议与__builtin_expect
一起玩,以获得更详细的答案。