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


348

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

我知道它们是为了优化,但是它们如何工作?使用它们会导致多少性能/尺寸下降?至少在瓶颈代码中(当然在用户空间中)值得麻烦(并且可能失去可移植性)。


7
这确实不是特定于Linux内核或宏,而是编译器优化。是否应该重新标记以反映这一点?
科迪·布罗维奇

11
本文每个程序员都应该知道什么内存(第57页)包含一个深入的解释。
Torsten Marek

2
另请参阅BOOST_LIKELY
Ruggero Turra 2015年

4
相关:关于使用基准的__builtin_expect另一个问题。
YSC

13
没有可移植性问题。你可以平凡做这样的事情#define likely(x) (x),并#define unlikely(x) (x)在不支持这种提示的平台。
David Schwartz

Answers:


328

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

像所有这样的性能优化一样,您应该仅在进行广泛的性能分析后才能执行此操作,以确保代码确实处于瓶颈,并且可能考虑到微观性质,因此代码正在紧密循环中运行。通常,Linux开发人员都非常有经验,所以我想他们会做到的。他们并不十分在乎可移植性,因为它们只针对gcc,并且对要生成的程序集有着非常密切的了解。


3
这些宏主要用于错误检查。因为错误留下的可能性比正常操作少。一些人进行剖析或计算以决定最常用的叶子...
Givenkoa 2012年

51
关于片段"[...]that it is being run in a tight loop",许多CPU都有分支预测器,因此使用这些宏仅有助于第一次执行代码或历史记录表被具有相同索引的不同分支覆盖到分支表中时使用。在一个紧密的循环中,假设分支在大多数情况下都以一种方式进行,则分支预测器可能会非常迅速地开始猜测正确的分支。-你在学脚上的朋友。
罗斯·罗杰斯

8
@RossRogers:真正发生的是编译器安排分支,因此常见的情况是未采用。即使分支预测确实起作用,这也更快。即使正确预测了分支,分支对于指令的获取和解码也是成问题的。一些CPU静态地预测不在其历史记录表中的分支,通常假定未采用正向分支。英特尔CPU不能这样工作:它们不会尝试检查预测变量表条目是否为此分支使用,而是无论如何都使用它。一个热分支和一个冷分支可能会将相同的条目作为别名...
Peter Cordes

12
这个答案通常已经过时了,因为主要的主张是它有助于分支预测,正如@PeterCordes指出的那样,在大多数现代硬件中,没有隐式或显式的静态分支预测。实际上,无论是涉及静态分支提示还是任何其他类型的优化,编译器都使用提示来优化代码。对于大多数架构的今天,它是“任何其他优化”的事项,例如,使得热路径是连续的,更好的调度炎热的路径,最大限度地减少了慢速路径的大小,只有矢量化的预期路径,等等,等等
BeeOnRope

3
由于缓存预取和字长,@ BeeOnRope具有线性运行程序的优势。下一个内存位置将已经被获取,并且在缓存中,分支目标可能会也可能不会。使用64位CPU时,一次至少要获取64位。取决于DRAM交错,它可能是2x 3x或更多的位。
布莱斯

88

让我们反编译以查看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由其他的答案中提到,并返回提高分支预测。

因此,它基本上与以下内容相同:

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的可能/不太可能属性他们可能(做一个双关语)会做同样的事情。


71

这些是宏,可向编译器提示分支可能的运行方式。如果可用,宏会扩展为GCC特定的扩展。

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

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

return x;

然后,可以将这段代码重组为类似以下内容的代码:

if (!x) {
  return x;
}

dosomething();
return x;

这样做的好处是,当处理器第一次执行分支时,会产生很大的开销,因为它可能是在推测性地加载和执行代码。当它确定将进入分支时,则必须使其无效,并从分支目标开始。

现在,大多数现代处理器都具有某种分支预测功能,但这仅在您之前浏览过分支并且分支仍位于分支预测缓存中时才有帮助。

在这些情况下,编译器和处理器可以使用许多其他策略。您可以在Wikipedia上找到有关分支预测变量如何工作的更多详细信息:http : //en.wikipedia.org/wiki/Branch_predictor


3
此外,它还会通过将不太可能的代码片段保留在热路径之外而影响icache占用空间。
fche

2
更准确地说,它可以使用gotos进行操作,而无需重复执行以下操作return xstackoverflow.com/a/31133787/895245
Ciro Santilli郝海东冠状病六四事件法轮功

7

它们使编译器在硬件支持它们的地方发出适当的分支提示。这通常仅意味着在指令操作码中旋转几位,因此代码大小不会改变。CPU将开始从预测的位置获取指令,并刷新管道并在到达分支后发现错误的情况下重新开始;否则,CPU将重新启动管道。在提示正确的情况下,这将使分支速度更快-确切的速度取决于硬件;对代码性能的影响程度将取决于时间提示的正确比例。

例如,在PowerPC CPU上,无提示的分支可能需要16个周期,正确提示的分支可能为8,而错误提示的分支可能为24。在最内层的循环中,良好的提示可能会产生巨大的影响。

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


3
出于记录目的,x86确实为分支提示占用了额外的空间。您必须在分支上有一个一字节的前缀才能指定适当的提示。同意提示是一件好事(TM)。
科迪·布罗西

2
Dang CISC CPU及其可变长度指令;)
moonshadow

3
Dang RISC
CPU-

7
@CodyBrocious:P4引入了分支提示,但与P4一起被放弃。所有其他x86 CPU只会忽略这些前缀(因为在毫无意义的上下文中始终会忽略前缀)。这些宏不会导致gcc在x86上实际发出分支提示前缀。它们确实可以帮助您使gcc在快速路径上使用较少的分支来布置功能。
彼得·科德斯

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))

参考:https : //www.akkadia.org/drepper/cpumemory.pdf


1
正如在对另一个答案的评论中所要求的那样-宏中双重反转的原因是什么(即,为什么使用__builtin_expect(!!(expr),0)而不是仅仅使用__builtin_expect((expr),0)
Michael Firth,

1
@MichaelFirth“双重倒置” !!等效于将某些内容强制转换为bool。有些人喜欢这样写。
本XO

2

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

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

您始终可以选择创建一个简单的nil效果“内联”或宏,使您可以使用其他编译器在其他平台上进行编译。

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


1
您不使用可移植性-不支持它们的平台只是将它们定义为扩展为空字符串。
sharptooth 2011年

2
我认为你们两个实际上是相互同意的-这只是令人困惑的措辞。(从表面上看,安德鲁的评论说“您可以使用它们而不会失去便携性”,但是夏普齿认为他说“不要使用它们,因为它们不便于携带”)并表示反对。)
Miral

2

根据Cody的评论,这与Linux无关,但这是对编译器的提示。发生什么情况将取决于体系结构和编译器版本。

Linux中的此特定功能在驱动程序中有些误用。正如osgx指出在热属性的语义,任何hotcold在块中调用功能可以自动提示该条件可能与否。例如,dump_stack()被标记cold为多余,

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

的未来版本gcc可能会根据这些提示有选择地内联函数。也有建议说不是boolean,而是分数,如最有可能的等等。通常,应该首选使用诸如的其他替代机制cold。除了热路径之外,没有其他理由可以使用它。编译器将在一种架构上执行的操作在另一种架构上可能会完全不同。


2

在许多linux发行版中,您可以在/ usr / linux /中找到complier.h,可以将其包括在内以供简单使用。还有另一种观点,可能性()比可能性()更有用,因为

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

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

顺便说一句,如果您想观察代码的详细行为,则可以简单地执行以下操作:

gcc -c test.c objdump -d test.o> obj.s

然后,打开obj.s,即可找到答案。


1

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

编辑:忘记了他们实际上可以真正提供帮助的地方。它可以允许编译器对控制流图进行重新排序,以减少为“可能”路径采取的分支数量。在检查多个退出案例的循环中,这可以显着改善。


10
gcc永远不会生成x86分支提示-至少所有Intel CPU都会忽略它们。但是,它将通过避免内联和循环展开来尝试在不太可能的区域中限制代码大小。
亚历克斯怪异

1

这些是GCC函数,供程序员使用,以向编译器提示有关给定表达式中最有可能发生分支条件的情况。这使编译器可以构建分支指令,以便最常见的情况是执行最少数量的指令。

分支指令的构建方式取决于处理器体系结构。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.