我一直在挖掘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开发人员都很有经验,所以我想他们会这样做。他们并不太关心可移植性,因为他们只针对gcc,他们非常清楚他们想要生成的程序集。

其他提示

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

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

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

return x;

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

if (!x) {
  return x;
}

dosomething();
return x;

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

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

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

让我们反编译看看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    
if (__builtin_expect(i, 0))
x8,%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
0000000000000000 <main>:
   0:       48 83 ec 08             sub    
int i = !time(NULL);
if (i)
    goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;
x8,%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 <*>x0,%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 <*>x8,%rsp 20: c3 retq 21: ba 01 00 00 00 mov <*>x1,%edx 26: be 00 00 00 00 mov <*>x0,%esi 27: R_X86_64_32 .rodata.str1.1 2b: bf 01 00 00 00 mov <*>x1,%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>
x1,%edx 15: be 00 00 00 00 mov <*>x0,%esi 16: R_X86_64_32 .rodata.str1.1 1a: bf 01 00 00 00 mov <*>x1,%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 <*>x0,%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 <*>x8,%rsp 34: c3 retq

内存中的指令顺序没有改变:首先是 printf 然后 puts retq 返回。

使用 __ builtin_expect

现在将 if(i)替换为:

<*>

我们得到:

<*>

printf (编译为 __ printf_chk )移动到函数的最后,在 puts 之后,返回改进分支预测如其他答案所述。

所以它基本上与:

相同 <*>

未使用 -O0 完成此优化。

但祝你写一个运行速度比 __ builtin_expect 更快的例子,而不是没有, CPU非常聪明天。我的天真尝试在这里

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

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

可移植性并不是真正的问题 - 可能是定义是在每个平台的标题中;你可以简单地定义“可能”和“不太可能”对于不支持静态分支提示的平台,没有任何内容。

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

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

您没有理由因使用它们而失去便携性。

您始终可以选择创建简单的无效“内嵌”效果。或者允许您在其他平台上与其他编译器一起编译的宏。

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

根据 Cody 的评论,这与Linux无关,但是暗示了编译器。发生的情况取决于架构和编译器版本。

Linux中的这一特殊功能在驱动程序中有些误用。正如 osgx 指出 hot属性的语义,在块中调用的任何 hot cold 函数都可以自动暗示条件是否可能。例如, dump_stack()被标记为 cold ,因此这是多余的,

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

gcc 的未来版本可以根据这些提示选择性地内联函数。还有一些建议认为它不是 boolean ,而是中的得分最可能是等。一般来说,应该首选使用一些替代机制,如。没有理由在任何地方使用它,而是使用热路径。编译器在一个架构上的作用在另一个架构上可能完全不同。

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

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

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

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

  

gcc -c test.c   objdump -d test.o&gt; obj.s

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

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

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top