Linux内核中可能的()/不太可能的()macros – 它们是如何工作的? 他们有什么好处?

我一直在挖掘Linux内核的一些部分,并发现这样的调用:

if (unlikely(fd < 0)) { /* Do something */ } 

要么

 if (likely(!err)) { /* Do something */ } 

我find了他们的定义:

 #define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0) 

我知道他们是为了优化,但他们是如何工作的? 使用它们可以预期性能/尺寸会下降多less? 至less在瓶颈代码(当然是在用户空间),这是否值得麻烦(并且可能丢失可移植性)。

它们是编译器发出的指令,会导致分支预测支持跳转指令的“可能”一方。 这可能是一个很大的胜利,如果预测是正确的,这意味着跳转指令基本上是空闲的,并且将花费零周期。 另一方面,如果预测是错误的,那么意味着处理器stream水线需要被刷新,并且可能花费几个周期。 只要预测在大部分时间是正确的,这将会对性能有好处。

就像所有这些性能优化一样,只有在广泛的性能分析之后,才能确保代码真正处于瓶颈状态,并且可能具有微观性质,因此它正在紧密地运行。 一般来说,Linux开发人员都非常有经验,所以我想他们会做到这一点。 他们并不太关心可移植性,因为他们只是针对gcc,而且他们对自己希望生成的程序集非常了解。

这些macros是给编译器提供分支可能走哪条路的提示。 如果可用,这些macros扩展到GCC特定扩展。

GCC使用这些来优化分支预测。 例如,如果你有以下的东西

 if (unlikely(x)) { dosomething(); } return x; 

那么它可以重构这个代码,更像是:

 if (!x) { return x; } dosomething(); return x; 

这样做的好处是,当处理器第一次接受分支时,会有很大的开销,因为它可能已经推测性地加载和执行代码。 当它确定将采取分支时,则必须使分支无效,并从分支目标开始。

大多数现代处理器现在都有某种分支预测,但只有当你经历过分支时才会有帮助,而分支仍然在分支预测caching中。

编译器和处理器可以在这些场景中使用许多其他策略。 您可以在维基百科上find有关分支预测器的更多详细信息: http : //en.wikipedia.org/wiki/Branch_predictor

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

没有__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) printf("%d\n", 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 14 jne 24 <main+0x24> 10: ba 01 00 00 00 mov $0x1,%edx 15: be 00 00 00 00 mov $0x0,%esi 16: R_X86_64_32 .rodata.str1.1 1a: bf 01 00 00 00 mov $0x1,%edi 1f: e8 00 00 00 00 callq 24 <main+0x24> 20: R_X86_64_PC32 __printf_chk-0x4 24: bf 00 00 00 00 mov $0x0,%edi 25: R_X86_64_32 .rodata.str1.1+0x4 29: e8 00 00 00 00 callq 2e <main+0x2e> 2a: R_X86_64_PC32 puts-0x4 2e: 31 c0 xor %eax,%eax 30: 48 83 c4 08 add $0x8,%rsp 34: c3 retq 

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

__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 11 je 21 <main+0x21> 10: bf 00 00 00 00 mov $0x0,%edi 11: R_X86_64_32 .rodata.str1.1+0x4 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 21: ba 01 00 00 00 mov $0x1,%edx 26: be 00 00 00 00 mov $0x0,%esi 27: R_X86_64_32 .rodata.str1.1 2b: bf 01 00 00 00 mov $0x1,%edi 30: e8 00 00 00 00 callq 35 <main+0x35> 31: R_X86_64_PC32 __printf_chk-0x4 35: eb d9 jmp 10 <main+0x10> 

printf (编译为__printf_chk )被移动到函数的最后, puts和返回以改善分支预测,如其他答案所述。

所以它基本上是一样的:

 int i = !time(NULL); if (i) goto printf; puts: puts("a"); return 0; printf: printf("%d\n", i); goto puts; 

这个优化不是用-O0完成的。

但是,如果用__builtin_expect写一个运行速度比没有运行速度更快的例子,那么在那些日子里 , CPU真的很聪明 。 我天真的尝试在这里 。

它们使编译器在硬件支持它们的地方发出适当的分支提示。 这通常只是指令操作码中的几位,所以代码大小不会改变。 CPU将开始从预测位置读取指令,并在到达分支时刷新stream水线并重新开始; 在提示正确的情况下,这将使分支更快 – 精确到多快取决于硬件; 以及影响代码性能的程度取决于时间提示的比例是多less。

例如,在一个PowerPC的CPU上,一个无阻塞的分支可能需要16个周期,一个正确的提示8和一个不正确的提示。在最内层的循环中,好的提示可以产生巨大的差异。

可移植性不是一个真正的问题 – 大概这个定义是在每个平台的头部; 对于不支持静态分支提示的平台,您可以简单地将“可能”和“不太可能”定义为“无”。

它们提示编译器在分支上生成提示前缀。 在x86 / x64上,它们占用一个字节,所以每个分支最多只能增加一个字节。 至于性能,完全取决于应用程序 – 现在大多数情况下,处理器上的分支预测器将忽略它们。

编辑:忘了一个地方,他们实际上可以帮助。 它可以允许编译器对控制stream图进行重新sorting,以减less“可能”path采用的分支数量。 这可以在你检查多个退出情况的循环中有显着的改进。

在很多linux发行版中,你可以在/ usr / linux /下findcomplier.h,你可以简单地使用它。 而另一个意见,不太可能()更有用,而不是可能(),因为

 if ( likely( ... ) ) { doSomething(); } 

它也可以在许多编译器中进行优化。

顺便说一句,如果你想观察代码的细节行为,你可以简单地做如下:

gcc -c test.c objdump -d test.o> obj.s

然后,打开obj.s,你可以find答案。

(一般评论 – 其他答案涵盖的细节)

没有理由使用它们来丢失可移植性。

你总是可以select创build一个简单的无效“内联”或macros,这将允许你在其他平台上编译其他编译器。

如果你在其他平台上,你不会得到优化的好处。

根据Cody的评论,这与Linux无关,但是对编译器是一个暗示。 发生什么将取决于体系结构和编译器版本。

Linux中的这个特殊function在驱动程序中有些误用。 由于osgx指出了hot属性的语义 ,任何在块中调用的hotcold函数都可以自动提示该条件可能与否。 例如, dump_stack()被标记为cold所以这是多余的,

  if(unlikely(err)) { printk("Driver error found. %d\n", err); dump_stack(); } 

未来版本的gcc可以根据这些提示select性地内联一个函数。 也有人认为这不是boolean ,而是最有可能的分数等等。一般来说,应该优先使用一些像cold一样的替代机制。 没有理由在任何地方使用它,但热path。 编译器在一个体系结构上做什么可以在另一个体系上完全不同。

 long __builtin_expect(long EXP, long C); 

这个结构告诉编译器,EXPexpression式最可能会有值C.返回值是EXP。 __builtin_expect是用来在条件expression式中使用的。 在几乎所有的情况下,它将被用在布尔expression式的上下文中,在这种情况下定义两个辅助macros会更方便:

 #define unlikely(expr) __builtin_expect(!!(expr), 0) #define likely(expr) __builtin_expect(!!(expr), 1) 

这些macros然后可以用于

 if (likely(a > 1)) 

参考: https : //www.akkadia.org/drepper/cpumemory.pdf

这些是GCC函数,程序员可以向编译器提供有关在给定expression式中最有可能的分支条件的提示。 这允许编译器构build分支指令,以便最常见的情况下执行最less数量的指令。

如何构build分支指令取决于处理器体系结构。