C++关键字之likely和unlikely

简介: C++关键字之likely和unlikely

流水线技术


现代CPU为了提高执行指令执行的吞吐量,使用了流水线技术,它将每条指令分解为多步,让不同指令的各步操作重叠,从而实现若干条指令并行处理。在流水线中,一条指令的生命周期可能包括:


  • 取指:将指令从存储器中读取出来,放入指令缓冲区中。


  • 译码:对取出来的指令进行翻译


  • 执行:知晓了指令内容,便可使用CPU中对应的计算单元执行该指令


  • 访存:将数据从存储器读出,或写入存储器


  • 写回:将指令的执行结果写回到通用寄存器组


流水线技术无法提升CPU执行单条指令的性能,但是可以通过相邻指令的并行化提高整体执行指令的吞吐量。




分支预测



我们都知道,程序的控制流程基本可分为三种:顺序、分支和循环。对CPU流水线来说,顺序比较好处理,一条路往前趟就行了。但是当程序中有了分支结构之后,CPU无法确切知道到底应该取分支1中的D指令,还是分支二中的E指令。


此时CPU会根据指令执行的上下文,猜测那一路分支应该被执行。预测的结果有两个,命中或者命不中。


在前一种情况下,CPU流水线正常执行,不会被打断。在后一种情况下,需要CPU丢掉为跳转指令之后的所有指令所做的工作,再开始从正确位置处起始的指令去填充流水线,这会导致很严重的惩罚:大约20-40个时钟周期的浪费,导致程序性能的严重下降。


1446762f051f1cdf0e6686932350e117.png



什么是likely和unlikely


既然程序是我们程序员所写,在一些明确的场景下,我们应该比CPU和编译器更了解哪个分支条件更有可能被满足。我们是否可将这一先验知识告知编译器和CPU, 提高分支预测的准确率,从而减少CPU流水线分支预测错误带来的性能损失呢?答案是可以!它便是likelyunlikely。在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去掉,然后反汇编,作为对照组


#include <cstdio>
#define likely(x)       __builtin_expect(!!(x), 1)
#define unlikely(x)     __builtin_expect(!!(x), 0)
int main(int argc, char *argv[])
{
    if (argc > 0) {
        puts ("Positive\n");
    } else
    {
        puts ("Zero or Negative\n");
    }
    return 0;
}


汇编如下,我们看到,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



因此,通过对分支条件使用likelyunlikely,我们可给编译器一种暗示,即该分支条件被满足的概率比较大或比较小。而编译器利用这一信息优化其机器指令,从而最大限度减少CPU分支预测失败带来的惩罚。




likely/unlikely的适用条件


CPU有自带的分支预测器,在大多数场景下效果不错。因此在分支发生概率严重倾斜、追求极致性能的场景下,使用likely/unlikely才具有较大意义。




C++20中的likely/unlikely


C++20之前的,likelyunlikely只不过是一对自定义的宏。而C++20中正式将likelyunlikely确定为属性关键字。


int foo(int i) {
    switch(i) {
               case 1: handle1();
                       break;
    [[likely]] case 2: handle2();
                       break;
    }
}


相关文章
|
1月前
|
存储 安全 编译器
【C++专栏】C++入门 | auto关键字、范围for、指针空值nullptr
【C++专栏】C++入门 | auto关键字、范围for、指针空值nullptr
40 0
|
2天前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
32 5
|
6天前
|
存储 编译器 程序员
C++一分钟之-auto关键字与类型推导
【6月更文挑战第21天】`auto`在C++11中重生,简化了类型声明,尤其在处理复杂类型时。它让编译器根据初始化值推导变量类型,减少了冗余和错误。使用`auto`简化了迭代器声明和函数返回类型推导,但也带来挑战:类型推导可能不直观,未初始化的`auto`是错误的,且过度使用影响可读性。使用`auto&`和`auto*`明确引用和指针,`decltype`辅助复杂类型推导,保持适度使用以维持代码清晰。
17 1
|
22天前
|
存储 安全 编译器
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
13 2
|
2天前
|
Unix 编译器 C语言
【C++航海王:追寻罗杰的编程之路】关键字、命名空间、输入输出、缺省、重载汇总
【C++航海王:追寻罗杰的编程之路】关键字、命名空间、输入输出、缺省、重载汇总
5 0
|
3天前
|
存储 安全 编译器
【C++】:函数重载,引用,内联函数,auto关键字,基于范围的for循环,nullptr关键字
【C++】:函数重载,引用,内联函数,auto关键字,基于范围的for循环,nullptr关键字
9 0
|
3天前
|
编译器 C语言 C++
【C++】:C++关键字,命名空间,输入&输出,缺省参数
【C++】:C++关键字,命名空间,输入&输出,缺省参数
10 0
|
27天前
|
C++
C++中使用namespace关键字定义和访问命名空间的技术性探讨
C++中使用namespace关键字定义和访问命名空间的技术性探讨
16 3
|
1月前
|
程序员 C++
为什么c++要引入class关键字
总之,C++引入 `class`关键字是为了支持面向对象编程,通过封装、继承、多态和抽象等特性,提供了更强大、灵活和可维护的编程工具,使得程序开发更加高效和可扩展。这使C++成为一种强大的编程语言,广泛用于各种应用领域。
37 1
|
1月前
|
C语言 C++
【C++入门】关键字、命名空间以及输入输出
【C++入门】关键字、命名空间以及输入输出