根据我在大学课程中所听到的,按照惯例,在 if
中放置更可能的条件而不是在 else
中放置更可能的条件可以帮助静态分支预测器。例如:
if (check_collision(player, enemy)) { // very unlikely to be true
doA();
} else {
doB();
}
可能被重写为:
if (!check_collision(player, enemy)) {
doB();
} else {
doA();
}
我发现了一篇博客文章《分支模式,使用GCC》,其中更详细地解释了这种现象:
if语句会生成向前分支。使它们不太可能被执行的理由是:处理器可以利用分支指令后面的指令已经被放置在指令单元内的指令缓冲区中这个事实。
它旁边还写着 (强调是我的):
编写if-else语句时,始终使“then”块比else块更容易执行,这样处理器就可以利用已经放置在指令获取缓冲区中的指令。
最终,Intel撰写了一篇文章《预防错误预测的分支和循环重组》,总结出两条规则:
当微处理器遇到分支时,在没有收集到数据的情况下使用静态分支预测,通常是第一次遇到分支时。规则很简单:
- 向前分支默认为未执行
- 向后分支默认为已执行
为了有效地编写代码以利用这些规则,在编写if-else或switch语句时,应首先检查最常见的情况,并逐步向下处理最不常见的情况。
据我所知,这个想法是流水线CPU可以按照指令缓存中的指令来执行代码段,而不会通过跳到代码段中的另一个地址来打破它。然而,我知道现代CPU微体系结构可能要复杂得多。
然而,看起来GCC没有遵循这些规则。考虑以下代码:
extern void foo();
extern void bar();
int some_func(int n)
{
if (n) {
foo();
}
else {
bar();
}
return 0;
}
它生成的是(版本6.3.0,使用-O3 -mtune=intel
):
some_func:
lea rsp, [rsp-8]
xor eax, eax
test edi, edi
jne .L6 ; here, forward branch if (n) is (conditionally) taken
call bar
xor eax, eax
lea rsp, [rsp+8]
ret
.L6:
call foo
xor eax, eax
lea rsp, [rsp+8]
ret
我发现强制执行期望行为的唯一方法是通过使用__builtin_expect
来重写 if
条件:
if (__builtin_expect(!!(condition), 1)) {
// expected behavior
} else {
// unexpected behavior
}
if (__builtin_expect(n, 1)) { // force n condition to be treated as true
因此,汇编代码将变为:
some_func:
lea rsp, [rsp-8]
xor eax, eax
test edi, edi
je .L2 ; here, backward branch is (conditionally) taken
call foo
xor eax, eax
lea rsp, [rsp+8]
ret
.L2:
call bar
xor eax, eax
lea rsp, [rsp+8]
ret
__builtin_expect
来告诉它哪个更有可能。或者更好的是,使用基于性能分析的优化。 - Ross Ridge-fdump-tree-all-all
)。通常情况下,它有一个启发式规则,即“==”更可能是false,但似乎在这里没有使用。您可以在gcc的bugzilla上提出问题来询问相关情况。请注意,如果您使用-fprofile-generate
编译程序,然后运行程序,再使用-fprofile-use
重新编译,则gcc将获得实际统计数据并做出更好的决策。 - Marc Glisse