当一个简单的方法是使用if-else时,为什么我们要使用__builtin_expect

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

该文件说:

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

您可以使用__builtin_expect为编译器提供分支预测信息。 一般来说,您应该更喜欢使用实际的configuration文件反馈( -fprofile-arcs ),因为程序员在预测其程序的实际执行方式方面出了名的糟糕。 但是,有些应用程序难以收集这些数据。

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

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

会表明我们不希望称为foo ,因为我们预期x为零。

那么为什么不直接使用:

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

而不是复杂的语法与期望?

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

 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 case在foo case之前(而不是C代码)。 这可以更好地利用CPUstream水线,因为跳转会使已获取的指令崩溃。

执行跳转之前,下面的指令( bar )被推送到pipe道。 由于foo情况不太可能发生,所以跳跃也不太可能,因此不太可能导致stream水线的颠簸。

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

我想,有人认为他们很聪明,他们正在加速这样做。

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

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

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

  • 你有一个非常真实的性能问题
  • 您已经适当地优化了系统中的algorithm
  • 你有性能数据来支持你的断言,一个特定的情况是最有可能的

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

Blagovest提到了分支反转来改进stream水线,但目前的编译器真的做到了吗? 让我们找出来!

没有__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 

内存中的指令顺序不变:首先是retq ,然后是retq返回。

__builtin_expect

现在, if (i)replace为:

 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那天真的很聪明 。 我天真的尝试在这里 。

那么,正如它在描述中所说的那样,第一个版本在构造中增加了一个预测元素,告诉编译器x == 0分支更有可能 – 也就是说,程序。

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

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

但是,如果所讨论的条件是一个紧密的内部循环的一部分,那么我只希望这个优化有明显的效果,因为结果代码中的差异相对较小。 如果你错误地优化它,你可能会降低你的performance。

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

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

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

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

如果编译器假定“真”更有可能,那么它可以优化不调用foo()

这里的问题只是你一般不知道编译器会采用什么 – 所以使用这种技术的任何代码都需要仔细测量(如果上下文发生变化,可能会随时监控)。