Linux内核中可能/不可能的宏是如何工作的,它们有什么好处?

我一直在深入研究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)

我知道它们是为了优化,但它们是如何工作的呢?使用它们可以减少多少性能/大小?至少在瓶颈代码中(当然是在用户空间中),这样做是否值得(可能还会失去可移植性)。

189540 次浏览

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

编辑:忘了一个他们能真正帮上忙的地方。它可以允许编译器重新排序控制流图,以减少“可能”路径的分支数量。在检查多个退出情况的循环中,这可以有显著的改进。

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

如何构建分支指令取决于处理器架构。

它们是对编译器发出指令的提示,这些指令将导致分支预测倾向于跳转指令的“可能”一侧。这可能是一个巨大的胜利,如果预测是正确的,这意味着跳跃指令基本上是免费的,将采取零周期。另一方面,如果预测是错误的,那么这意味着处理器管道需要刷新,这可能会花费几个周期。只要预测在大多数时候是正确的,这将有利于性能。

就像所有这样的性能优化一样,你应该只在广泛的分析之后进行,以确保代码确实处于瓶颈中,并且可能考虑到微观性质,它正在一个紧密的循环中运行。一般来说,Linux开发人员都很有经验,所以我可以想象他们会这样做。他们不太关心可移植性,因为他们只针对gcc,而且他们对希望生成的程序集有非常接近的想法。

它们导致编译器在硬件支持它们的地方发出适当的分支提示。这通常只意味着在指令操作码中摆弄几位,所以代码大小不会改变。CPU将开始从预测的位置获取指令,并在到达分支时,如果结果是错误的,则刷新管道并重新开始;在提示正确的情况下,这将使分支更快-确切地说,多快将取决于硬件;这对代码性能的影响程度取决于时间提示的正确比例。

例如,在PowerPC CPU上,未提示的分支可能需要16个周期,正确提示的是8个周期,错误提示的是24个周期。在最内层循环中,良好的暗示可以产生巨大的差异。

可移植性并不是真正的问题——假设定义是在每个平台的头文件中;对于不支持静态分支提示的平台,可以简单地将“可能”和“不太可能”定义为零。

这些宏向编译器提供了关于分支可能走向的提示。如果这些宏可用,它们将扩展为特定于GCC的扩展。

GCC使用这些来优化分支预测。例如,如果您有以下内容

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


return x;

然后它可以重新构造这段代码,使其更像:

if (!x) {
return x;
}


dosomething();
return x;

这样做的好处是,当处理器第一次使用分支时,会有很大的开销,因为它可能已经预先加载和执行了代码。当它决定使用分支时,它必须使其无效,并从分支目标开始。

大多数现代处理器现在都有某种分支预测,但这只在您之前已经通过了分支,并且分支仍然在分支预测缓存中时才有帮助。

在这些场景中,编译器和处理器还可以使用许多其他策略。你可以在Wikipedia: http://en.wikipedia.org/wiki/Branch_predictor中找到关于分支预测器如何工作的更多细节

(一般评论-其他答案涵盖了细节)

没有理由因为使用它们而失去可移植性。

你总是可以选择创建一个简单的无效果“内联”或宏,这将允许你在其他平台上使用其他编译器进行编译。

如果您在其他平台上,则无法获得优化的好处。

在许多linux发行版中,你可以在/usr/linux/中找到compiler.h,你可以简单地将它包含进来使用。另一种观点是,unlikely()比likely()更有用,因为

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

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

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

gcc -c test.c Objdump -d测试。阿比;对象< / p >

然后,打开obj。S,你可以找到答案。

根据科迪的注释,这与Linux无关,但对编译器是一个提示。会发生什么取决于体系结构和编译器版本。

Linux中的这个特殊特性在驱动程序中被误用了。正如osgx热点属性语义中指出的那样,在块中调用的任何hotcold函数都可以自动提示条件可能存在或不存在。例如,dump_stack()被标记为cold,所以这是多余的,

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

gcc的未来版本可能会根据这些提示有选择地内联函数。也有人建议它不是boolean,而是像最有可能的一样的分数,等等。一般来说,应该优先使用一些替代机制,如cold。没有理由在炎热的道路以外的任何地方使用它。编译器在一种架构上的功能在另一种架构上可能完全不同。

让我们反编译,看看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,然后是putsretq返回。

__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和return之后,以改进其他答案中提到的分支预测。

所以基本上是一样的:

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比不用现在的cpu真的很智能运行得更快的例子。我天真的尝试在这里

c++ 20 [[likely]][[unlikely]]

c++ 20标准化了那些c++内置:如何在if-else语句中使用c++ 20's likely/unlikely属性它们可能(双关语!)做同样的事情。

long __builtin_expect(long EXP, long C);
这个构造告诉编译器表达式EXP . xml 最有可能的值是c。返回值是EXP。 __builtin_expect用于条件句 表达式。在几乎所有的情况下,它将被用于 上下文的布尔表达式在这种情况下,它是很多 更方便的定义两个helper宏:

#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