在if语句中,GCC的__builtin_expect有什么优势?

时间:2011-09-08 10:55:42

标签: c linux gcc built-in

我遇到#define,他们使用__builtin_expect

The documentation说:

  

内置函数:long __builtin_expect (long exp, long c)

     

您可以使用__builtin_expect为编译器提供分支   预测信息。一般来说,您应该更喜欢使用实际   正如程序员所做的那样(-fprofile-arcs)的个人资料反馈   众所周知,在预测他们的计划实际执行情况方面表现不佳。   但是,有些应用程序很难收集这些数据。

     

返回值是exp的值,它应该是一个整数   表达。内置的语义是预期的   exp == c。例如:

      if (__builtin_expect (x, 0))
        foo ();
     

表示我们不希望拨打foo,因为我们希望x为零。

那么为什么不直接使用:

if (x)
    foo ();

而不是__builtin_expect的复杂语法?

7 个答案:

答案 0 :(得分:153)

想象一下将从以下代码生成的汇编代码:

if (__builtin_expect(x, 0)) {
    foo();
    ...
} else {
    bar();
    ...
}

我想它应该是这样的:

  cmp   $x, 0
  jne   _foo
_bar:
  call  bar
  ...
  jmp   after_if
_foo:
  call  foo
  ...
after_if:

您可以看到说明的排列顺序是bar案例在foo案例之前(而不是C代码)。这可以更好地利用CPU管道,因为跳转会使已经获取的指令崩溃。

在执行跳转之前,它下面的指令(bar情况)被推送到管道。由于foo的情况不太可能发生,因此不太可能跳跃,因此不大可能打破管道。

答案 1 :(得分:39)

__builtin_expect的想法是告诉编译器你通常会发现表达式的计算结果为c,这样编译器就可以针对这种情况进行优化。

我猜有人认为他们很聪明,并且他们通过这样做加快了速度。

不幸的是,除非情况非常清楚(可能他们没有做过这样的事情),否则可能会让事情变得更糟。文档甚至说:

  

一般情况下,您应该更喜欢使用实际的个人资料反馈(-fprofile-arcs),因为程序员在预测他们的程序实际执行情况方面是出了名的。但是,有些应用程序很难收集这些数据。

一般情况下,您不应该使用__builtin_expect,除非:

  • 您有一个非常真实的性能问题
  • 您已经适当优化了系统中的算法
  • 您已获得性能数据来备份您的断言,即特定案例最有可能

答案 2 :(得分:38)

让我们反编译看看GCC 4.8用它做什么

Blagovest提到了分支反转以改善管道,但目前的编译器真的做到了吗?我们来看看吧!

没有__builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        puts("a");
    return 0;
}

使用GCC 4.8.2 x86_64 Linux编译和反编译:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

输出:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 0a                   jne    1a <main+0x1a>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq

内存中的指令顺序未更改:首先puts然后retq返回。

使用__builtin_expect

现在将if (i)替换为:

if (__builtin_expect(i, 0))

我们得到:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 07                   je     17 <main+0x17>
  10:       31 c0                   xor    %eax,%eax
  12:       48 83 c4 08             add    $0x8,%rsp
  16:       c3                      retq
  17:       bf 00 00 00 00          mov    $0x0,%edi
                    18: R_X86_64_32 .rodata.str1.1
  1c:       e8 00 00 00 00          callq  21 <main+0x21>
                    1d: R_X86_64_PC32       puts-0x4
  21:       eb ed                   jmp    10 <main+0x10>

puts已移至函数的最后,retq返回!

新代码基本上与:

相同
int i = !time(NULL);
if (i)
    goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;

未使用-O0进行此优化。

但祝你好好编写一个使用__builtin_expect而不是CPUs are really smart those days的运行速度更快的示例。我的天真尝试are here

答案 3 :(得分:13)

好吧,正如在描述中所说,第一个版本在构造中添加了一个预测元素,告诉编译器x == 0分支更可能是一个 - 也就是说,它是将要采用的分支更常见的是你的程序。

考虑到这一点,编译器可以优化条件,以便在预期条件成立时需要最少量的工作,代价是在意外情况下可能需要做更多的工作。

看看在编译阶段以及在结果程序集中如何实现条件,以查看一个分支如何比另一个分支更少工作。

但是,如果有问题的条件是被称为 lot 的紧密内循环的一部分,我只希望这种优化具有明显的效果,因为结果代码的差异相对较小。如果你以错误的方式优化它,你可能会降低你的表现。

答案 4 :(得分:1)

我没有看到任何解决我认为你问的问题的答案,转述:

  

是否有一种更便携的方法可以将分支预测提示给编译器。

你问题的标题让我想到这样做:

if ( !x ) {} else foo();

如果编译器假设'true'更有可能,则可以优化不调用foo()

这里的问题只是你通常不知道编译器会假设什么 - 所以任何使用这种技术的代码都需要仔细测量(并且如果上下文发生变化,可能会随着时间的推移进行监控)。

答案 5 :(得分:1)

我根据@Blagovest Buyukliev和@Ciro在Mac上进行了测试。程序集看起来很清晰,我添加了注释;

命令是 gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o

当我使用-O3时,无论__builtin_expect(i,0)是否存在,它看起来都一样。

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp     
0000000000000001    movq    %rsp, %rbp    // open function stack
0000000000000004    xorl    %edi, %edi       // set time args 0 (NULL)
0000000000000006    callq   _time      // call time(NULL)
000000000000000b    testq   %rax, %rax   // check time(NULL)  result
000000000000000e    je  0x14           //  jump 0x14 if testq result = 0, namely jump to puts
0000000000000010    xorl    %eax, %eax   //  return 0   ,  return appear first 
0000000000000012    popq    %rbp    //  return 0
0000000000000013    retq                     //  return 0
0000000000000014    leaq    0x9(%rip), %rdi  ## literal pool for: "a"  // puts  part, afterwards
000000000000001b    callq   _puts
0000000000000020    xorl    %eax, %eax
0000000000000022    popq    %rbp
0000000000000023    retq

使用-O2进行编译时,无论是否带有__builtin_expect(i,0),它看起来都不同

首先没有

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    jne 0x1c       //   jump to 0x1c if not zero, then return
0000000000000010    leaq    0x9(%rip), %rdi ## literal pool for: "a"   //   put part appear first ,  following   jne 0x1c
0000000000000017    callq   _puts
000000000000001c    xorl    %eax, %eax     // return part appear  afterwards
000000000000001e    popq    %rbp
000000000000001f    retq

现在带有__builtin_expect(i,0)

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    je  0x14   // jump to 0x14 if zero  then put. otherwise return 
0000000000000010    xorl    %eax, %eax   // return appear first 
0000000000000012    popq    %rbp
0000000000000013    retq
0000000000000014    leaq    0x7(%rip), %rdi ## literal pool for: "a"
000000000000001b    callq   _puts
0000000000000020    jmp 0x10

总而言之,__builtin_expect在最后一种情况下有效。

答案 6 :(得分:0)

在大多数情况下,您应该保持分支预测不变,而不必担心。

一种有益的情况是具有大量分支的CPU密集型算法。在某些情况下,跳转可能导致超出当前的CPU程序缓存,从而使CPU等待软件的下一部分运行。通过在末尾推送不太可能的分支,您将保持记忆关闭,仅在不太可能的情况下跳转。