7

考虑以下 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

主要是从跳跃等于跳跃不等于的变化。我知道直到 Pentium 4,处理器不考虑条件前向分支上的静态分支预测(似乎静态预测在其他英特尔处理器上变得随机),因此我想这种优化正在处理这个问题。

假设并参考jne优化版本,这意味着else块实际上被认为比程序流中的if块更有可能执行。

但这究竟是什么意思?由于编译器没有对foo函数中的a值进行假设,因此这种概率仅依赖于程序员的著作(实际上,他们可以使用而不是和反转函数调用)。if ( !a )if ( a )

这是否意味着将if条件块视为例外情况(而不是正常的执行流程)应该被视为一种好习惯?

那是:

if ( !cond ) {
    // exceptional code
}
else {
    // normal continuation
}

代替:

if ( cond ) {
    // normal continuation
}
else {
    // exceptional code
}

(当然,人们可能更喜欢在相关块内使用 return 语句来限制缩进大小)。

4

1 回答 1

4

我曾经在 ARM(7,9) 上进行过大量的性能优化操作。它是纯 C 语言,足够愚蠢的编译器(SDT AFAIR)。节省一些 CPU 资源的方法之一是分析if分支并重写if条件,以便正常流程不会破坏线性指令序列。由于 CPU 预测块更有效的使用和更有效的代码段内存缓存使用,这都有积极的影响。

我认为在这里我们看到了非常接近的优化。在第一个代码片段中,两个分支都导致正常序列被破坏(6一个分支和12另一个分支都带有 lavel)。在第二个片段中,一个分支指令被排序retq,其他分支序列具有单跳转(不比第一个片段中的差)。请注意2retq条说明。

因此,正如我所看到的,这不是块重新排序的问题,je而是jne块重新排序的问题,因此分支是线性指令序列,其中一个在没有任何jump和完全预测块功率的情况下输入。

关于“为什么 GCC 更喜欢一个分支而不是另一个分支”......我在文档中看到这可能是静态分支预测的结果(基于翻译单元内的调用?)。无论如何,我建议一起玩__builtin_expect以获得更详细的答案。

于 2013-09-01T16:58:43.550 回答