从我的大学课程中,我听说,根据惯例,最好在if
而不是else
中放置更多可能的条件,这可能有助于静态分支预测。例如:
if (check_collision(player, enemy)) { // very unlikely to be true
doA();
} else {
doB();
}
可以改写为:
if (!check_collision(player, enemy)) {
doB();
} else {
doA();
}
我找到了一篇博文Branch Patterns, Using GCC,它更详细地解释了这一现象:
为if语句生成转发分支。的理由 使它们不可能是处理器可以采取的 分支后面的指令的优点 指令可能已经放在了指令缓冲区内 指令单位。
旁边,它说(强调我的):
在编写if-else语句时,始终使"然后"阻止更多 可能比else块执行,所以处理器可以采取 已经放在指令获取中的指令的优点 缓冲液中。
最终,有一篇由英特尔撰写的文章Branch and Loop Reorganization to Prevent Mispredicts,其中总结了两条规则:
当没有数据收集时,使用静态分支预测 微处理器遇到分支时,通常是 第一次遇到分支。规则很简单:
- 正向分支默认为未采取
- 向后分支默认为已采取
为了有效地编写代码以利用这些代码 规则,在编写 if-else 或切换语句时,请检查最多 常见的情况首先发生,并逐渐减少到最不常见的情况。
据我所知,想法是流水线CPU可以遵循指令缓存中的指令而不会通过跳转到代码段内的另一个地址来破坏它。但是,我知道,在现代CPU微体系结构的情况下,这可能会过于简单。
然而,看起来GCC并不尊重这些规则。鉴于代码:
extern void foo();
extern void bar();
int some_func(int n)
{
if (n) {
foo();
}
else {
bar();
}
return 0;
}
它生成(带有-O3 -mtune=intel
的版本6.3.0):
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(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
答案 0 :(得分:3)
答案简短:不,不是。
GCC做了大量非平凡优化,其中一个是通过控制流图来判断分支概率。
根据GCC manual:
FNO猜测分支概率
不要猜测分支概率 试探法。
如果不是,GCC会使用启发式方法来猜测分支概率 通过分析反馈(
-fprofile-arcs
)提供。这些启发式是 基于控制流程图。如果有一些分支概率 由__builtin_expect
指定,然后启发式用于猜测 取其余控制流图的分支概率 考虑__builtin_expec
t信息。之间的相互作用 启发式和__builtin_expect
可能很复杂,在某些情况下也是如此 可能有助于禁用启发式,以便的效果__builtin_expect
更容易理解。
-freorder-blocks
也可以交换分支。
此外,正如OP所提到的,行为可能会被__builtin_expect
覆盖。
请查看以下列表。
void doA() { printf("A\n"); }
void doB() { printf("B\n"); }
int check_collision(void* a, void* b)
{ return a == b; }
void some_func (void* player, void* enemy) {
if (check_collision(player, enemy)) {
doA();
} else {
doB();
}
}
int main() {
// warming up gcc statistic
some_func((void*)0x1, NULL);
some_func((void*)0x2, NULL);
some_func((void*)0x3, NULL);
some_func((void*)0x4, NULL);
some_func((void*)0x5, NULL);
some_func(NULL, NULL);
return 0;
}
很明显,check_collision
大部分时间都会返回0
。因此,doB()
分支很可能,GCC可以猜到这一点:
gcc -O main.c -o opt.a
objdump -d opt.a
some_func
的asm是:
sub $0x8,%rsp
cmp %rsi,%rdi
je 6c6 <some_func+0x18>
mov $0x0,%eax
callq 68f <doB>
add $0x8,%rsp
retq
mov $0x0,%eax
callq 67a <doA>
jmp 6c1 <some_func+0x13>
但可以肯定的是,我们可以通过过于聪明来强制执行GCC:
gcc -fno-guess-branch-probability main.c -o non-opt.a
objdump -d non-opt.a
我们会得到:
push %rbp
mov %rsp,%rbp
sub $0x10,%rsp
mov %rdi,-0x8(%rbp)
mov %rsi,-0x10(%rbp)
mov -0x10(%rbp),%rdx
mov -0x8(%rbp),%rax
mov %rdx,%rsi
mov %rax,%rdi
callq 6a0 <check_collision>
test %eax,%eax
je 6ef <some_func+0x33>
mov $0x0,%eax
callq 67a <doA>
jmp 6f9 <some_func+0x3d>
mov $0x0,%eax
callq 68d <doB>
nop
leaveq
retq
因此,GCC将按来源顺序离开分支机构。
我使用gcc 7.1.1进行测试。
答案 1 :(得分:-1)
有趣的是 空间 和 没有 优化的优化是 仅 生成“最佳”指令代码的情况:gcc -S [-O0 | -Os] source.c
some_func:
FB0:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
cmpl $0, 8(%ebp)
je L2
call _foo
jmp L3
2:
call _bar
3:
movl $0, %eax
# Or, for -Os:
# xorl %eax, %eax
leave
ret
我的观点是......
some_func:
FB0:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
cmpl $0, 8(%ebp)
je L2
call _foo
......直到&amp;通过调用foo
,传统意义上的一切都是“最优的”,无论退出策略如何。
当然,最优性最终由处理器决定。