当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《Linux内核中可能/不太可能的宏如何工作以及它们的好处是什么?》经验,为你挑选了5个好方法。

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

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



1> 1800 INFORMA..:

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

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


关于片段`"[...]它正在紧密循环中运行",许多CPU都有[分支预测器](http://en.wikipedia.org/wiki/Branch_predictor),因此使用这些宏只能帮助第一次执行代码,或者当历史表被具有相同索引的不同分支覆盖到分支表中时.在紧密循环中,并且假设分支在大多数时间以单向进行,分支预测器可能会很快开始猜测正确的分支. - 你的朋友在迂腐.
这个答案大多已经过时,因为主要的主张是它有助于分支预测,正如@PeterCordes所指出的,在大多数现代硬件中,没有隐式或显式静态分支预测.实际上,编译器使用提示来优化代码,无论是涉及静态分支提示还是任何其他类型的优化.对于今天的大多数体系结构来说,重要的是"任何其他优化",例如,使热路径连续,更好地调度热路径,最小化慢速路径的大小,仅向量化预期路径等等.
@RossRogers:真正发生的是编译器安排分支,因此常见的情况是未采用的分支.即使分支预测确实有效,这也会更快.采取分支对于指令获取和解码是有问题的,即使它们被完美地预测.某些CPU静态地预测不在其历史表中的分支,通常假设不采用前向分支.Intel CPU不能以这种方式工作:它们不会尝试检查预测器表条目是否为*this*分支,它们只是使用它.热分支和冷分支可能是同一个条目的别名......
这些宏主要用于错误检查.因为错误可能比正常操作少.一些人进行分析或计算以决定最常用的叶子......
@BeeOnRope由于缓存预取和字大小,线性运行程序仍然有优势.下一个内存位置已经被提取,并且在缓存中,分支目标可能是也可能不是.使用64位CPU,您一次至少可以获得64位.根据DRAM交错,它可能是2x 3x或更多位被抓取.

2> Ciro Santill..:

让我们反编译看看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 
: 0: 48 83 ec 08 sub $0x8,%rsp 4: 31 ff xor %edi,%edi 6: e8 00 00 00 00 callq b 7: R_X86_64_PC32 time-0x4 b: 48 85 c0 test %rax,%rax e: 75 14 jne 24 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 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 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 
: 0: 48 83 ec 08 sub $0x8,%rsp 4: 31 ff xor %edi,%edi 6: e8 00 00 00 00 callq b 7: R_X86_64_PC32 time-0x4 b: 48 85 c0 test %rax,%rax e: 74 11 je 21 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 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 31: R_X86_64_PC32 __printf_chk-0x4 35: eb d9 jmp 10

printf(编译__printf_chk)被转移到功能的尽头,之后puts,并返回提高分支预测由其他的答案中提到.

所以它基本上是相同的:

int i = !time(NULL);
if (i)
    goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;

这种优化没有完成-O0.

但是,写一个运行速度__builtin_expect比没有运行得更快的例子,祝你好运,那些时候CPU非常聪明.我天真的尝试就在这里.



3> dvorak..:

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

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

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

return x;

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

if (!x) {
  return x;
}

dosomething();
return x;

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

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

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


此外,它还会影响icache足迹 - 通过将不可能的代码片段保留在热门路径之外.
更准确地说,它可以使用`goto`s而不重复`return x`来实现:http://stackoverflow.com/a/31133787/895245

4> moonshadow..:

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

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

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


@CodyBrocious:P4推出了分支提示,但与P4一起被抛弃.所有其他x86 CPU都会忽略这些前缀(因为前缀总是在它们没有意义的上下文中被忽略).这些宏*不会导致gcc在x86上实际发出分支提示前缀.它们可以帮助你让gcc在快速路径上使用较少的分支来布置你的功能.
为了记录,x86确实为分支提示占用了额外的空间.您必须在分支上具有一个单字节前缀以指定适当的提示.但同意暗示是一件好事(TM).
Dang RISC CPU - 远离我的15字节指令;)
Dang CISC CPU及其可变长度指令;)

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

推荐阅读
乐韵答题
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有