我一直在研究 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)
我知道它们是为了优化,但它们是如何工作的?使用它们可以减少多少性能/尺寸?至少在瓶颈代码中(当然是在用户空间中)是否值得麻烦(并且可能失去可移植性)。
BOOST_LIKELY
__builtin_expect
在另一个问题上。
#define likely(x) (x)
和 #define unlikely(x) (x)
之类的操作。
它们暗示编译器发出指令,这些指令将导致分支预测有利于跳转指令的“可能”一侧。这可能是一个巨大的胜利,如果预测正确,则意味着跳转指令基本上是免费的,并且将占用零个周期。另一方面,如果预测错误,则意味着需要刷新处理器流水线,并且可能会花费几个周期。只要预测在大多数情况下都是正确的,这往往对性能有好处。
像所有这样的性能优化一样,您应该只在进行广泛的分析后才进行它,以确保代码确实处于瓶颈中,并且可能考虑到微观性质,它正在一个紧密的循环中运行。一般来说,Linux 开发人员非常有经验,所以我想他们会这样做。他们并不真正关心可移植性,因为他们只针对 gcc,并且他们对他们希望它生成的程序集有一个非常接近的想法。
让我们反编译看看 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 main() {
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 these days 运行得更快的示例。我天真的尝试are here。
C++20 [[likely]]
和 [[unlikely]]
C++20 已经标准化了那些 C++ 内置函数:How to use C++20's likely/unlikely attribute in if-else statement 他们可能会(双关语!)做同样的事情。
这些是向编译器提示分支可能走向的宏。如果可用,宏会扩展到 GCC 特定的扩展。
GCC 使用这些来优化分支预测。例如,如果您有以下内容
if (unlikely(x)) {
dosomething();
}
return x;
然后它可以将这段代码重组为更像:
if (!x) {
return x;
}
dosomething();
return x;
这样做的好处是,当处理器第一次进行分支时,会有很大的开销,因为它可能已经推测性地加载和执行更前面的代码。当它确定它将采用分支时,它必须使该分支无效,并从分支目标开始。
大多数现代处理器现在都有某种分支预测,但这仅在您之前经历过分支并且分支仍在分支预测缓存中时才有帮助。
在这些场景中,编译器和处理器可以使用许多其他策略。您可以在 Wikipedia 上找到有关分支预测器如何工作的更多详细信息:http://en.wikipedia.org/wiki/Branch_predictor
它们使编译器在硬件支持的地方发出适当的分支提示。这通常只是意味着在指令操作码中旋转几位,因此代码大小不会改变。 CPU 将开始从预测的位置获取指令,并在到达分支时刷新管道并在发现错误时重新开始;在提示正确的情况下,这将使分支更快——确切的速度取决于硬件;而这对代码性能的影响程度将取决于时间提示正确的比例。
例如,在 PowerPC CPU 上,一个未提示的分支可能需要 16 个周期,正确提示的一个 8 周期和一个错误提示的 24 个周期。在最内层循环中,良好的提示可以产生巨大的差异。
可移植性并不是真正的问题——大概定义在每个平台的标题中;对于不支持静态分支提示的平台,您可以简单地将“可能”和“不太可能”定义为空。
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))
参考:https://www.akkadia.org/drepper/cpumemory.pdf
__builtin_expect(!!(expr),0)
而不仅仅是 __builtin_expect((expr),0)
?
!!
相当于将某些东西转换为 bool
。有些人喜欢这样写。
(一般性评论 - 其他答案涵盖了细节)
没有理由因为使用它们而失去可移植性。
您始终可以选择创建一个简单的零效应“内联”或宏,以允许您使用其他编译器在其他平台上进行编译。
如果您在其他平台上,您将无法从优化中受益。
根据 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
。除了热路径之外,没有理由在任何地方使用它。编译器在一种架构上所做的事情在另一种架构上可能完全不同。
在许多linux发行版中,您可以在/usr/linux/
中找到compiler.h
,您可以简单地包含它以供使用。另一种观点是,不太可能()比可能()更有用,因为
if ( likely( ... ) ) {
doSomething();
}
它也可以在许多编译器中进行优化。
顺便说一句,如果您想观察代码的详细行为,您可以简单地执行以下操作:
gcc -c test.c objdump -d test.o > obj.s
然后,打开obj.s,就可以找到答案了。
它们是编译器在分支上生成提示前缀的提示。在 x86/x64 上,它们占用一个字节,因此每个分支最多会增加一个字节。至于性能,它完全取决于应用程序——在大多数情况下,处理器上的分支预测器会忽略它们,这些天。
编辑:忘记了一个他们实际上可以提供帮助的地方。它可以允许编译器重新排序控制流图,以减少为“可能”路径采用的分支数量。这可以显着改善您检查多个退出案例的循环。
这些是供程序员使用的 GCC 函数,用于向编译器提示在给定表达式中最可能出现的分支条件。这允许编译器构建分支指令,以便在最常见的情况下执行最少数量的指令。
如何构建分支指令取决于处理器架构。
"[...]that it is being run in a tight loop"
,许多 CPU 都有一个 branch predictor,因此使用这些宏仅有助于第一次执行代码或历史表被具有相同索引的不同分支覆盖到分支表时。在一个紧密的循环中,假设一个分支大部分时间都是单向的,分支预测器可能会很快开始猜测正确的分支。 - 你的迂腐朋友。