正如描述中所说,第一个版本在构造中添加了一个预测元素,告诉编译器 x == 0分支更有可能——也就是说,这个分支将更经常地被你的程序使用。

考虑到这一点,编译器可以优化条件,以便在预期条件保持不变的情况下,它所需要的工作量最少,代价是在出现意外条件的情况下,可能不得不做更多的工作。

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

但是,如果所讨论的条件是一个紧密的内部循环的一部分,而这个内部循环被称为 很多,那么我只希望这种优化能够产生显著的效果,因为得到的代码之间的差异相对较小。如果你的优化方式不对,你的表现可能会下降。

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

我猜有人觉得他们很聪明他们这么做是为了加快进度。

不幸的是,除非情况是 非常明白(很可能他们没有这样做) ,否则很可能会使事情变得更糟。文件甚至说:

一般来说,您应该更喜欢使用实际的配置文件反馈(-fprofile-arcs) ,因为程序员在预测他们的程序实际执行情况方面是出了名的糟糕。但是,在一些应用程序中很难收集这些数据。

一般来说,你不应该使用 __builtin_expect,除非:

  • 你的表现真的很差
  • 您已经适当地优化了系统中的算法
  • 您已经获得了性能数据来支持您的断言,即某个特定情况最有可能发生

想象一下,从以下方面生成汇编代码:

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的情况是不可能的,跳跃也是不可能的,因此打击管道是不可能的。

我没有看到任何回答我认为你在问的问题的答案:

是否有更便携的方式向编译器提示分支预测。

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

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

如果编译器假设‘ true’更有可能,那么它可以优化不调用 foo()

这里的问题是,一般情况下,您不知道编译器将会采用什么样的方法——所以任何使用这种技术的代码都需要仔细测量(如果上下文发生变化,可能还需要随时监视)。

让我们反编译看看 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的可能/不可能属性,他们很可能(一个双关语!)做同样的事情。

我根据@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 在最后一种情况下可以工作。

在大多数情况下,您应该保持分支预测的原样,不必担心它。

其中有益的一种情况是 CPU 密集型算法带有大量分支。在某些情况下,跳转可能导致超过当前的 CPU 程序缓存,使 CPU 等待软件的下一部分运行。通过把不太可能的分支推到最后,你将保持记忆封闭,只在不太可能的情况下跳跃。