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


144

我碰到了一个#define他们用的__builtin_expect

该文档说:

内置功能: 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



3
我认为您的直接代码应该是if ( x == 0) {} else foo();..或简单地if ( x != 0 ) foo();等同于GCC文档中的代码。
Nawaz

Answers:


186

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

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
真的那样工作吗?为什么foo定义不能先出现?就原型而言,函数定义的顺序无关紧要,对吧?
kingsmasher11年

63
这与函数定义无关。它是关于重新安排机器代码,以使CPU提取将不执行的指令的可能性较小。
Blagovest Buyukliev 2011年

4
哦,我明白了。因此,您的意思是,由于这样做的可能性很高,x = 0因此优先考虑该标准。还有foo,稍后定义,因为它的机会(而不是使用概率)较小,对吗?
kingsmasher1 2011年

1
啊..谢谢 那是最好的解释。汇编代码确实成功了:)
kingsmasher1 2011年

5
这也可能为CPU 分支预测器嵌入提示,改善流水线化
Hasturkun 2011年

50

让我们反编译以查看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比没有时运行得更快的示例,那时候CPU确实很聪明。我的天真尝试在这里

C ++ 20 [[likely]][[unlikely]]

C ++ 20已经标准化了那些C ++内置函数:如何在if-else语句中使用C ++ 20的可能/不太可能属性


1
检验libdispatch的dispatch_once函数,该函数使用__builtin_expect进行实际优化。慢速路径曾经运行过一次,并利用__builtin_expect提示分支预测变量应该采用快速路径。快速路径完全不使用任何锁!mikeash.com/pyblog/…–
亚当·卡普兰

在GCC 9.2中似乎没有任何区别:gcc.godbolt.org/z/GzP6cx(实际上已经在8.1中)
Ruslan,

40

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

我猜想有人认为他们很聪明,并且这样做可以加快工作速度。

不幸的是,除非对此情况有充分的了解(很可能他们没有做过这样的事情),否则情况可能会变得更糟。该文档甚至说:

通常,您应该更喜欢为此(-fprofile-arcs)使用实际的配置文件反馈,因为众所周知,程序员在预测其程序的实际执行效果方面很差。但是,在某些应用程序中很难收集此数据。

通常,__builtin_expect除非:

  • 您有一个非常实际的性能问题
  • 您已经适当地优化了系统中的算法
  • 您已经获得了性能数据来支持您断定特定案例最有可能发生的说法

7
@Michael:这实际上不是分支预测的描述。
奥利弗·查尔斯沃思

3
“大多数程序员都是坏人”,或者总比编译器好。任何白痴都可以在for循环中看出延续条件可能是正确的,但是编译器也知道这一点,因此告诉它没有任何好处。如果由于某种原因您编写了一个几乎总是立即中断的循环,并且如果您无法为PGO的编译器提供概要数据,那么程序员可能知道编译器没有的东西。
史蒂夫·杰索普

15
在某些情况下,哪个分支更有可能无关紧要,而是哪个分支重要。如果意外分支导致abort(),则可能性无关紧要,并且在优化时应为预期分支赋予性能优先级。
Neowizard '02

1
与你的要求的问题是,优化CPU可以相对于分支的概率是相当多限于一个执行:分支预测,并且这种优化情况无论使用__builtin_expect与否。另一方面,编译器可以根据分支概率执行许多优化,例如组织代码以使热路径是连续的,移动不太可能进一步优化的代码或减小其大小,从而决定对哪些分支进行矢量化处理,更好地安排热路径,等等。
BeeOnRope

1
...没有开发者的信息,它是盲目的,选择了中立的策略。如果开发人员对概率是正确的(并且在许多情况下,了解通常采用/不采用分支是微不足道的)-您将获得这些好处。如果不是的话,您将受到一定的损失,但是它并没有比收益大多少,而且最关键的是,这些都不能超越 CPU分支预测。
BeeOnRope

13

嗯,正如其描述中所述,第一个版本在构造中添加了一个预测性元素,告诉编译器该x == 0分支更有可能出现-即,该分支将被程序更频繁地采用。

考虑到这一点,编译器可以优化条件,以便在预期条件成立时它需要最少的工作量,但可能会在遇到意外情况时进行更多工作。

看一下在编译阶段以及在结果汇编中如何实现条件,以了解一个分​​支可能比另一个分支少工作。

但是,我只希望如果所讨论的条件是一个紧密的内部循环(该循环被大量调用)的一部分,则此优化将具有显着效果,因为结果代码中的差异相对较小。而且,如果以错误的方式对其进行优化,则很可能会降低性能。


但是最后,所有这些都是关于由编译器检查条件的,您是否要说编译器始终假设该分支并继续进行,后来如果没有匹配项,那么呢?怎么了?我认为在编译器设计中还有一些关于分支预测的东西,以及它是如何工作的。
kingsmasher11年

2
这确实是一个微观优化。查看条件的实现方式,其中一个分支的偏差很小。作为一个假设的示例,假设条件成为测试加上程序集中的跳转。然后,跳转分支比非跳转分支慢,因此您希望将期望分支设为非跳转分支。
Kerrek SB 2011年

谢谢,您和Michael我认为观点相似,但用不同的词来表达:-)我知道关于“测试和分支”的确切编译器内部信息无法在这里解释:)
kingsmasher1 2011年

通过搜索互联网也很容易了解它们:-)
Kerrek SB 2011年

我最好回到我的大学书籍compiler design - Aho, Ullmann, Sethi:-)
kingsmasher11年

1

我看不出有任何答案可以解决我认为您在问的问题:

有没有一种更便携的方式向编译器提示分支预测。

您的问题标题使我想到了这种方式:

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

如果编译器认为“ true”的可能性更大,则可以针对不调用进行优化foo()

这里的问题是,您通常不知道编译器将要假设的内容,因此,使用这种技术的任何代码都需要进行仔细测量(如果上下文发生更改,则可能会随时间进行监视)。


实际上,这可能恰好是OP最初打算键入的内容(如标题所示),但是由于某种原因,使用的内容else被忽略了。
布伦特·布拉德本

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在最后一种情况下有效。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.