GCC是否为静态分支预测生成次优代码?

时间:2017-01-26 18:49:05

标签: c gcc assembly x86 branch-prediction

从我的大学课程中,我听说,根据惯例,最好在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

2 个答案:

答案 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,传统意义上的一切都是“最优的”,无论退出策略如何。

当然,最优性最终由处理器决定。