How do the likely/unlikely macros in the Linux kernel work and what is their benefit?
我一直在挖掘Linux内核的某些部分,发现这样的调用:
1 2 3 4 | if (unlikely(fd < 0)) { /* Do something */ } |
要么
1 2 3 4 | if (likely(!err)) { /* Do something */ } |
我找到了它们的定义:
1 2 | #define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0) |
我知道它们是为了优化,但它们是如何工作的? 使用它们可以预期性能/尺寸减少多少? 至少在瓶颈代码中(当然在用户空间中)是否值得麻烦(并且可能失去可移植性)。
它们暗示编译器发出的指令将导致分支预测有利于跳转指令的"可能"侧。这可能是一个巨大的胜利,如果预测是正确的,这意味着跳转指令基本上是免费的并且将采用零周期。另一方面,如果预测是错误的,则意味着需要刷新处理器流水线并且可能花费几个周期。只要预测在大多数情况下是正确的,这将有利于性能。
像所有这些性能优化一样,您应该只在进行大量分析后才能确保代码真正处于瓶颈状态,并且可能具有微观特性,即它在紧密循环中运行。通常Linux开发人员都很有经验,所以我想他们会这样做。他们并不太关心可移植性,因为他们只针对gcc,他们对他们想要生成的程序集非常了解。
这些是宏,它们向编译器提供有关分支可能采用的方式的提示。如果宏可用,宏将扩展为GCC特定扩展。
GCC使用这些来优化分支预测。例如,如果您有类似以下内容
1 2 3 4 5 | if (unlikely(x)) { dosomething(); } return x; |
然后它可以重构这个代码更像是:
1 2 3 4 5 6 | if (!x) { return x; } dosomething(); return x; |
这样做的好处是,当处理器第一次采用分支时,会产生很大的开销,因为它可能已经推测性地加载并进一步执行代码。当它确定它将采用分支时,它必须使其无效,并从分支目标开始。
大多数现代处理器现在都有某种分支预测,但这只会在您之前通过分支时提供帮助,并且分支仍在分支预测缓存中。
编译器和处理器可以在这些场景中使用许多其他策略。您可以在维基百科上找到有关分支预测变量如何工作的更多详细信息:http://en.wikipedia.org/wiki/Branch_predictor
让我们反编译看看GCC 4.8对它的作用
没有
1 2 3 4 5 6 7 8 9 10 11 12 | #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 ", i); puts("a"); return 0; } |
使用GCC 4.8.2 x86_64 Linux编译和反编译:
1 2 | gcc -c -O3 -std=gnu11 main.c objdump -dr main.o |
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 |
内存中的指令顺序未更改:首先是
用
现在将
1 | if (__builtin_expect(i, 0)) |
我们得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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> |
在
所以它基本上是相同的:
1 2 3 4 5 6 7 8 9 10 | int i = !time(NULL); if (i) goto printf; puts: puts("a"); return 0; printf: printf("%d ", i); goto puts; |
使用
但是写一个运行速度比
它们使编译器发出硬件支持它们的相应分支提示。这通常只意味着在指令操作码中篡改几位,因此代码大小不会改变。 CPU将开始从预测位置获取指令,并在达到分支时刷新管道并重新开始,如果结果是错误的话。在提示正确的情况下,这将使分支更快 - 确切地说,取决于硬件的速度有多快;以及这对代码性能的影响程度取决于时间提示的正确比例。
例如,在PowerPC CPU上,未打印的分支可能需要16个周期,正确提示的8个周期和错误提示的24个周期。在最里面的循环中,良好的提示可以产生巨大的差异。
可移植性并不是真正的问题 - 可能是定义在每个平台的标题中;您可以简单地为不支持静态分支提示的平台定义"可能"和"不太可能"。
1 | long __builtin_expect(long EXP, long C); |
此构造告诉编译器表达式EXP
最有可能是C值。返回值是EXP。
__builtin_expect旨在用于条件
表达。在几乎所有情况下它都会被用于
布尔表达式的上下文,在这种情况下它很多
更方便定义两个辅助宏:
1 2 | #define unlikely(expr) __builtin_expect(!!(expr), 0) #define likely(expr) __builtin_expect(!!(expr), 1) |
然后可以使用这些宏
1 | if (likely(a > 1)) |
参考:https://www.akkadia.org/drepper/cpumemory.pdf
根据Cody的评论,这与Linux无关,但它是对编译器的暗示。发生的情况取决于架构和编译器版本。
Linux中的这一特殊功能在驱动程序中有些误用。正如osgx在hot属性的语义中指出的那样,在块中调用的任何
1 2 3 4 5 | if(unlikely(err)) { printk("Driver error found. %d ", err); dump_stack(); } |
在很多linux版本中,你可以在/ usr / linux /中找到complier.h,你可以简单地包含它。而另一种意见,不太可能()更有用而不是可能(),因为
1 2 3 | if ( likely( ... ) ) { doSomething(); } |
它可以在许多编译器中进行优化。
顺便说一下,如果你想观察代码的细节行为,你可以简单地做如下:
gcc -c test.c
objdump -d test.o > obj.s
然后,打开obj.s,就可以找到答案。
(一般评论 - 其他答案涵盖细节)
你没有理由因使用它们而失去便携性。
您始终可以选择创建一个简单的无效"内联"或宏,以便您可以在其他平台上使用其他编译器进行编译。
如果你在其他平台上,你将无法获得优化的好处。
这些是GCC函数,程序员可以向编译器提供关于给定表达式中最可能的分支条件的提示。这允许编译器构建分支指令,以便最常见的情况需要执行最少数量的指令。
如何构建分支指令取决于处理器体系结构。
它们提示编译器在分支上生成提示前缀。在x86 / x64上,它们占用一个字节,因此每个分支最多可以增加一个字节。至于性能,它完全取决于应用程序 - 在大多数情况下,处理器上的分支预测器现在会忽略它们。
编辑:忘了他们实际上可以真正帮助的一个地方。它可以允许编译器对控制流图重新排序,以减少"可能"路径所采用的分支数。在您检查多个退出案例时,这可以显着改善循环。