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

kin*_*er1 130 c linux gcc built-in

我遇到了#define他们使用的一个__builtin_expect.

文件说:

内置功能: long __builtin_expect (long exp, long c)

您可以使用__builtin_expect为编译器提供分支预测信息.一般来说,你应该更喜欢使用实际的配置文件反馈(-fprofile-arcs),因为程序员在预测程序实际执行情况方面是非常糟糕的.但是,有些应用程序难以收集此数据.

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

      if (__builtin_expect (x, 0))
        foo ();
Run Code Online (Sandbox Code Playgroud)

表示我们不打算打电话foo,因为我们预计x会为零.

那么为什么不直接使用:

if (x)
    foo ();
Run Code Online (Sandbox Code Playgroud)

而不是复杂的语法__builtin_expect

Bla*_*iev 167

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

if (__builtin_expect(x, 0)) {
    foo();
    ...
} else {
    bar();
    ...
}
Run Code Online (Sandbox Code Playgroud)

我想它应该是这样的:

  cmp   $x, 0
  jne   _foo
_bar:
  call  bar
  ...
  jmp   after_if
_foo:
  call  foo
  ...
after_if:
Run Code Online (Sandbox Code Playgroud)

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

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

  • 这与功能定义无关.它是关于重新排列机器代码的方式,导致CPU获取不会执行的指令的可能性较小. (54认同)
  • 这也可以为CPU [分支预测器](http://en.wikipedia.org/wiki/Branch_predictor)嵌入提示,改进流水线操作 (5认同)
  • 哦,我明白了.所以你的意思是因为'x = 0'的概率很高所以首先给出条形.foo,后来被定义,因为它的机会(而不是使用概率)更少,对吧? (4认同)
  • 真的有这样的作用吗?为什么 foo 定义不能放在第一位?只要有原型,函数定义的顺序就无关紧要,对吧? (2认同)

Cir*_*四事件 42

让我们反编译看看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;
}
Run Code Online (Sandbox Code Playgroud)

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

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

内存中的指令顺序没有改变:首先puts然后retq返回.

__builtin_expect

现在替换if (i)为:

if (__builtin_expect(i, 0))
Run Code Online (Sandbox Code Playgroud)

我们得到:

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>
Run Code Online (Sandbox Code Playgroud)

puts被转移到功能,的最末端retq的回报!

新代码基本相同:

int i = !time(NULL);
if (i)
    goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;
Run Code Online (Sandbox Code Playgroud)

这种优化没有完成-O0.

但是,写一个运行速度__builtin_expect比没有运行得更快的例子,祝你好运,那些时候CPU非常聪明.我天真的尝试就在这里.

  • 查看 libdispatch 的dispatch_once 函数,它使用 __builtin_expect 进行实际优化。慢速路径只运行一次,并利用 __builtin_expect 来提示分支预测器应该采用快速路径。快速路径运行时根本不使用任何锁!https://www.mikeash.com/pyblog/friday-qa-2014-06-06-secrets-of-dispatch_once.html (2认同)

Mic*_*hne 41

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

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

不幸的是,除非情况得到很好的理解(很可能他们没有做过这样的事情),否则可能会让事情变得更糟.文档甚至说:

一般来说,你应该更喜欢使用实际的配置文件反馈(-fprofile-arcs),因为程序员在预测程序实际执行情况方面是非常糟糕的.但是,有些应用程序难以收集此数据.

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

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

  • 在某些情况下,哪个分支更有可能无关紧要,而是哪个分支更重要.如果意外分支导致abort(),则可能性无关紧要,并且在优化时应给予预期分支性能优先级. (13认同)
  • @Michael:这不是分支预测的描述. (7认同)
  • "大多数程序员都很糟糕"或者无论如何都不比编译器好.任何白痴都可以告诉你,在for循环中,延续条件可能是真的,但是编译器也知道这样做也没有任何好处.如果由于某种原因你写了一个几乎总是会立即中断的循环,并且如果你不能为PGO的编译器提供配置文件数据,*那么*也许程序员知道编译器没有的东西. (3认同)
  • 您声称的问题在于 CPU 可以执行的关于分支概率的优化几乎仅限于一种:分支预测,并且无论您是否使用 `__builtin_expect` _都会发生这种优化_。另一方面,编译器可以根据分支概率执行许多优化,例如组织代码以使热路径是连续的,将不太可能被优化的代码移到更远的地方或减小其大小,决定要向量化哪些分支,更好地调度热路径,等等。 (3认同)
  • ...没有来自开发人员的信息,它是盲目的并选择了中立的策略。如果开发人员对概率是正确的(并且在许多情况下,理解分支通常被采用/不采用是微不足道的) - 您将获得这些好处。如果你不是,你会得到一些惩罚,但它并不比好处大得多,最关键的是,这些都不会_覆盖_CPU分支预测。 (2认同)

Ker*_* SB 13

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

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

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

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

  • 这确实是一种微观优化.查看条件的实现方式,对一个分支有一点偏差.作为一个假设的例子,假设条件成为测试加上程序集中的跳转.然后跳跃分支比非跳跃分支慢,所以你更喜欢使预期分支成为非跳跃分支. (2认同)