C++关键字之likely和unlikely详解
作者:misaka-mikoto
什么是likely和unlikely
既然程序是我们程序员所写,在一些明确的场景下,我们应该比CPU和编译器更了解哪个分支条件更有可能被满足。我们是否可将这一先验知识告知编译器和CPU, 提高分支预测的准确率,从而减少CPU流水线分支预测错误带来的性能损失呢?答案是可以!它便是likely和unlikely。在Linux内核代码中,这两个宏的应用比比皆是。下面是他们的定义:
#define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)
likely,用于修饰if/else if分支,表示该分支的条件更有可能被满足。而unlikely与之相反
以下为示例。unlikely修饰argc > 0分支,表示该分支不太可能被满足。
#include <cstdio> #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) int main(int argc, char *argv[]) { if (unlikely(argc > 0)) { puts ("Positive\n"); } else { puts ("Zero or Negative\n"); } return 0; }
likely/unlikely的原理
接下来,我们从汇编指令分析likely/unlikely到底是如何起作用的?
首先我们将上述代码中的unlikely去掉,然后反汇编,作为对照组
汇编如下,我们看到,if分支中的指令被编译器放置于分支跳转指令jle相邻的位置,即CPU流水线在遇到jle指令所代表的的'岔路口'时,更倾向于走if分支
.LC0: .string "Positive\n" .LC1: .string "Zero or Negative\n" main: sub rsp, 8 test edi, edi jle .L2 ; 如果argc <= 0, 跳转到L2 mov edi, OFFSET FLAT:.LC0 ; 如果argc > 0, 从这里执行 call puts .L3: xor eax, eax add rsp, 8 ret .L2: mov edi, OFFSET FLAT:.LC1 call puts jmp .L3
接着我们在if分支中加上unlikely, 反汇编如下。这里的情况正好与对照组相反,if分支下的指令被编译器放置于远离跳转指令jg的位置。这意味着CPU此时更倾向于走else分支。
.LC0: .string "Positive\n" .LC1: .string "Zero or Negative\n" main: sub rsp, 8 test edi, edi jg .L6 mov edi, OFFSET FLAT:.LC1 call puts .L3: xor eax, eax add rsp, 8 ret .L6: mov edi, OFFSET FLAT:.LC0 call puts jmp .L3
因此,通过对分支条件使用likely和unlikely,我们可给编译器一种暗示,即该分支条件被满足的概率比较大或比较小。而编译器利用这一信息优化其机器指令,从而最大限度减少CPU分支预测失败带来的惩罚。
likely/unlikely的适用条件
CPU有自带的分支预测器,在大多数场景下效果不错。因此在分支发生概率严重倾斜、追求极致性能的场景下,使用likely/unlikely才具有较大意义。
C++20中的likely/unlikely
C++20之前的,likely和unlikely只不过是一对自定义的宏。而C++20中正式将likely和unlikely确定为属性关键字。
int foo(int i) { switch(i) { case 1: handle1(); break; [[likely]] case 2: handle2(); break; } }
到此这篇关于C++关键字之likely和unlikely详解的文章就介绍到这了,更多相关C++ likely和unlikely内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!