首页 文章

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

提问于
浏览
285

我一直在挖掘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)

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

10 回答

  • 2

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

    像所有这些性能优化一样,您应该只在进行大量分析后才能确保代码真正处于瓶颈状态,并且可能具有微观特性,即它在紧密循环中运行 . 通常Linux开发人员都很有经验,所以我想他们会这样做 . 他们并不太关心可移植性,因为他们只针对gcc,他们对他们想要生成的程序集非常了解 .

  • 2

    这些是宏,它们向编译器提供关于分支可能采用的方式的提示 . 如果宏可用,宏将扩展为GCC特定扩展 .

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

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

    然后它可以重构这个代码更像是:

    if (!x) {
      return x;
    }
    
    dosomething();
    return x;
    

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

    大多数现代处理器现在都有某种分支预测,但这只会在您之前通过分支时提供帮助,并且分支仍在分支预测缓存中 .

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

  • 6

    Let's decompile to see what GCC 4.8 does with it

    Without __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 返回 .

    With __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>
    

    puts 之后, printf (编译为 __printf_chk )被移动到函数的最末端,并且返回以改进其他答案所提到的分支预测 .

    所以它基本上是相同的:

    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 those days好运 . 我天真的尝试are here .

  • 4

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

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

    可移植性并不是一个真正的问题 - 可能是定义在每个平台的 Headers 中;您可以简单地为不支持静态分支提示的平台定义“可能”和“不太可能” .

  • 1
    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

  • 65

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

    你没有理由因使用它们而失去便携性 .

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

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

  • 2

    根据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 这样的替代机制 . 没有理由在任何地方使用它,但热路径 . 编译器在一个架构上的作用在另一个架构上可能完全不同 .

  • 265

    在许多linux发行版中,你可以在/ usr / linux /中找到complier.h,你可以简单地包含它 . 而另一种意见,不太可能()更有用而不是可能(),因为

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

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

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

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

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

  • 59

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

    编辑:忘了一个他们真正可以帮助的地方 . 它可以允许编译器对控制流图重新排序,以减少“可能”路径所采用的分支数 . 在您检查多个退出案例时,这可以显着改善循环 .

  • 1

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

    如何构建分支指令取决于处理器体系结构 .

相关问题