likely(x) 和 __builtin_expect((x),1)

9
我知道内核大量使用likelyunlikely宏。这些宏的文档位于内置函数:long __builtin_expect (long exp, long c)。但是,文档并没有详细讨论细节。
编译器如何处理likely(x)__builtin_expect((x),1)
它是由代码生成器还是优化器处理的?
它是否取决于优化级别?
有什么代码生成的示例?

为什么不检查生成的代码呢?注意:你看不到任何特别之处。如果你理解那些“宏”所做的事情,就应该清楚为什么了... - Karoly Horvath
1
这是一个相当复杂的问题,但基本上编译器生成的机器代码将进行一些针对特定目标的微小更改,以使分支预测器和缓存以某种方式运行。至少对于x86架构,最好查看英特尔的处理器文档以了解具体细节。 - Richard J. Ross III
@jww:这不是一次对话。 - Karoly Horvath
4
学习阅读编译器生成的汇编代码非常有用,可以帮助理解程序的运行情况。特别是当你对微小优化感兴趣时更是如此。 - Art
1
是的,我也认同Art所说的。出于这个原因,我正在学习x86和ARM汇编语言远比我想象中要多。 - Daniel Santos
1个回答

13

我刚在gcc上进行了一个简单的测试。

对于x86,这似乎由优化器处理,并且取决于优化级别。尽管我猜正确的答案是“这取决于编译器”。

生成的代码是CPU相关的。一些CPU(例如sparc64)在条件分支指令上有标志,告诉CPU如何预测它,因此编译器根据编译器内置规则和代码提示(例如__builtin_expect)生成“预测为真/预测为假”指令。

英特尔在这里说明了它们的行为:https://software.intel.com/en-us/articles/branch-and-loop-reorganization-to-prevent-mispredicts。简而言之,在英特尔CPU上的行为是,如果CPU没有关于分支的先前信息,它会将向前的分支预测为不太可能被采用,而向后的分支则可能被采用(考虑循环与错误处理)。

这是一些示例代码:

int bar(int);
int
foo(int x)
{
    if (__builtin_expect(x>10, PREDICTION))
        return bar(10);
    return 42;
}

编译使用的选项(我使用omit-frame-pointer选项使输出更易读,但下面我还是整理了一下):

$ cc -S -fomit-frame-pointer -O0 -DPREDICTION=0 -o 00.s foo.c
$ cc -S -fomit-frame-pointer -O0 -DPREDICTION=1 -o 01.s foo.c
$ cc -S -fomit-frame-pointer -O2 -DPREDICTION=0 -o 20.s foo.c
$ cc -S -fomit-frame-pointer -O2 -DPREDICTION=1 -o 21.s foo.c

00.s和01.s之间没有区别,这表明这取决于优化(至少对于gcc来说是这样)。

下面是20.s的生成代码(经过简化处理):

foo:
    cmpl    $10, %edi
    jg  .L2
    movl    $42, %eax
    ret
.L2:
    movl    $10, %edi
    jmp bar

这里是21.s:

foo:
    cmpl    $10, %edi
    jle .L6
    movl    $10, %edi
    jmp bar
.L6:
    movl    $42, %eax
    ret

正如预期的那样,编译器重新排列了代码,以便我们不期望执行的分支是在前向分支中完成的。


1
谢谢Art。我不知道x86中有前向/后向分支“提示”!此外,当您的“冷”代码位于函数末尾(在生成的代码中)时,除非需要,否则可能永远不会加载到CPU的缓存中,这是一件好事。哦,是的,jww,请了解coldhot属性。我相信include/linux/compiler.h也有它们的宏。如果将函数标记为此类,则可以省略(不)可能的宏。 - Daniel Santos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接