静态分支预测 / GCC 优化

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 之前,处理器认为条件向前跳转的静态分支预测不会被采用(似乎在后来的 Intel 处理器上,静态预测变成了随机预测),因此我想这种优化是针对这个问题而进行的。

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

但这到底意味着什么?由于编译器在 foo 函数中没有对 a 值做任何假设,因此这种概率仅依赖于程序员的编写(实际上程序员可以使用 if ( !a ) 而不是 if ( a ) 并反转函数调用)。

这是否意味着应该将 if 条件块视为异常情况(而不是正常执行流程)?

也就是说:

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

改为:

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

(当然,人们可以更喜欢在相关块内使用返回语句来限制缩进大小。)
1个回答

4
我曾在ARM(7,9)上进行了大量性能优化的操作。那是纯C语言,编译器非常简单(我记得是SDT)。其中一种节省CPU资源的方法是分析if分支,并重写if条件,使正常流程不会打破线性指令序列。这对于CPU预测块更有效的使用和更高效的代码段内存缓存使用都有积极的影响。
我认为这里看到的是非常接近的优化。在第一个代码片段中,两个分支都导致正常序列被打破(一个分支带有标签6的行,另一个分支带有标签12的行)。在第二个片段中,一个分支的指令排序到retq,而另一个分支序列只有单个跳转(不比第一个片段更糟)。请注意2个retq指令。
因此,我认为这不是jejne的问题,而是块重新排序的问题,使得分支成为线性指令序列,其中一个进入没有任何jump的状态,并且完全预测块功率节省。
关于“为什么GCC更喜欢一个分支而不是另一个分支”...我在文档中看到这可能是静态分支预测的结果(基于翻译单元内部的调用?)。无论如何,我建议使用__builtin_expect来得到更详细的答案。

是的...但是不同的返回点也可以在je版本上实现。GCC有意识地重新排列if/else块:在初始程序中将if(a)更改为if(!a),编译结果完全相反:从jne(非优化)版本到je(分支顺序优化)版本。我不敢相信GCC只是为了取笑我而做出这种改变! :) - lledr
我认为在任何情况下,__builtin_expect都应该有所帮助 ;-). http://blog.man7.org/2012/10/how-much-do-builtinexpect-likely-and.html - Roman Nikitchenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接