ChatGPT解决这个技术问题 Extra ChatGPT

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)

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

这实际上不是特定于 Linux 内核或宏,而是编译器优化。是否应该重新标记以反映这一点?
论文 What every Programmer should know about Memory (p. 57) 包含深入的解释。
另见BOOST_LIKELY
相关:a benchmark on the use of __builtin_expect 在另一个问题上。
不存在便携性问题。您可以在不支持这种提示的平台上简单地执行 #define likely(x) (x)#define unlikely(x) (x) 之类的操作。

j
jww

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

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


这些宏主要用于错误检查。因为错误离开的可能性低于正常操作。少数人进行分析或计算来确定最常用的叶子......
关于片段 "[...]that it is being run in a tight loop",许多 CPU 都有一个 branch predictor,因此使用这些宏仅有助于第一次执行代码或历史表被具有相同索引的不同分支覆盖到分支表时。在一个紧密的循环中,假设一个分支大部分时间都是单向的,分支预测器可能会很快开始猜测正确的分支。 - 你的迂腐朋友。
@RossRogers:真正发生的是编译器排列分支,因此常见情况是未采用的情况。即使分支预测确实有效,这也更快。即使可以完美地预测,所采用的分支对于指令获取和解码也是有问题的。一些 CPU 静态预测不在其历史表中的分支,通常假设未采用前向分支。英特尔 CPU 不是这样工作的:它们不会尝试检查预测表条目是否适用于该分支,它们只是使用它。热分支和冷分支可能为同一个条目起别名......
这个答案大部分已经过时了,因为主要声称它有助于分支预测,正如@PeterCordes 指出的那样,在大多数现代硬件中,没有隐式或显式的静态分支预测。事实上,编译器使用提示来优化代码,无论是涉及静态分支提示还是任何其他类型的优化。对于当今的大多数架构,重要的是“任何其他优化”,例如,使热路径连续,更好地调度热路径,最小化慢速路径的大小,仅矢量化预期路径等。
@BeeOnRope 由于缓存预取和字长,线性运行程序仍然有优势。下一个内存位置将已经被获取并在缓存中,分支目标可能会也可能不会。使用 64 位 CPU,您一次至少可以抓取 64 位。根据 DRAM 交错,可能会被抓取 2x 3x 或更多位。
L
LustreOne

让我们反编译看看 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 他们可能会(双关语!)做同样的事情。


u
user545424

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

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

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

return x;

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

if (!x) {
  return x;
}

dosomething();
return x;

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

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

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


此外,它会影响 icache 占用空间 - 通过将不太可能的代码片段排除在热路径之外。
更准确地说,它可以使用 goto 来完成,而无需重复 return xstackoverflow.com/a/31133787/895245
m
moonshadow

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

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

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


作为记录,x86 确实为分支提示占用了额外的空间。您必须在分支上有一个单字节前缀才能指定适当的提示。不过,同意暗示是一件好事(TM)。
该死的 CISC CPU 及其可变长度指令;)
Dang RISC CPU——远离我的 15 字节指令;)
@CodyBrocious:P4 引入了分支提示,但与 P4 一起被放弃了。所有其他 x86 CPU 都只是忽略这些前缀(因为前缀在无意义的上下文中总是被忽略)。这些宏不会导致 gcc 在 x86 上实际发出分支提示前缀。它们确实可以帮助您让 gcc 在快速路径上使用更少的分支来布置您的功能。
A
Ashish Maurya
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)
@MichaelFirth“双重反转”!! 相当于将某些东西转换为 bool。有些人喜欢这样写。
A
Andrew Edgecombe

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

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

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

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


您不使用可移植性 - 不支持它们的平台只是将它们定义为扩展为空字符串。
我认为你们两个实际上是相互同意的——只是措辞令人困惑。 (从表面上看,Andrew 的评论是说“你可以使用它们而不会失去便携性”,但Sharptooth 认为他说“不要使用它们,因为它们不便携”并表示反对。)
C
Community

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

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

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

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


F
Fox

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

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

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

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

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

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


S
Serafina Brocious

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

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


gcc 从不生成 x86 分支提示 - 至少所有 Intel CPU 无论如何都会忽略它们。不过,它将尝试通过避免内联和循环展开来限制不太可能区域的代码大小。
d
dcgibbons

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

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


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅