411

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

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

4

10 回答 10

381

它们暗示编译器发出指令,这些指令将导致分支预测有利于跳转指令的“可能”一侧。这可能是一个巨大的胜利,如果预测正确,则意味着跳转指令基本上是免费的,并且将占用零个周期。另一方面,如果预测错误,则意味着需要刷新处理器流水线,并且可能会花费几个周期。只要预测在大多数情况下都是正确的,这往往对性能有好处。

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

于 2008-09-20T23:09:26.847 回答
116

让我们反编译看看 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>

( printfcompiled to __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 的可能/不太可能属性他们很可能(双关语!)做同样的事情。

于 2015-06-30T08:56:40.753 回答
79

这些是向编译器提示分支可能走向的宏。如果可用,宏会扩展到 GCC 特定的扩展。

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

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

return x;

然后它可以将这段代码重组为更像:

if (!x) {
  return x;
}

dosomething();
return x;

这样做的好处是,当处理器第一次进行分支时,会有很大的开销,因为它可能已经推测性地加载和执行更前面的代码。当它确定它将采用分支时,它必须使该分支无效,并从分支目标开始。

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

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

于 2008-09-20T23:14:39.580 回答
11

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

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

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

于 2008-09-20T23:11:53.267 回答
7
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

于 2016-11-23T13:22:05.497 回答
4

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

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

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

如果您在其他平台上,您将无法从优化中受益。

于 2008-09-20T23:19:44.153 回答
3

根据Cody的评论,这与 Linux 无关,而是对编译器的提示。会发生什么取决于架构和编译器版本。

Linux 中的这一特殊功能在驱动程序中有些误用。正如osgx在hot attribute的语义中指出的那样,在块中调用的任何函数hotcold函数都可以自动提示该条件可能与否。例如,dump_stack()被标记cold所以这是多余的,

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

的未来版本gcc可能会根据这些提示选择性地内联函数。也有人建议它不是boolean,而是最有可能的分数等。通常,应该首选使用一些替代机制,如cold. 除了热路径之外,没有理由在任何地方使用它。编译器在一种架构上所做的事情在另一种架构上可能完全不同。

于 2014-01-27T20:59:36.603 回答
2

在许多 linux 发行版中,您可以在 /usr/linux/ 中找到 complier.h,您可以简单地包含它以供使用。另一种观点是,不太可能()比可能()更有用,因为

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

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

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

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

然后,打开obj.s,就可以找到答案了。

于 2012-03-07T02:27:04.490 回答
1

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

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

于 2008-09-20T23:07:35.493 回答
1

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

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

于 2008-09-20T23:08:15.243 回答