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
,然后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 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属性的语义 ,任何在块中调用的hot
或cold
函数都可以自动提示该条件可能与否。 例如, 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分支指令取决于处理器体系结构。