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