目录

流水线技术

分支预测

什么是likely和unlikely

likely/unlikely的原理

likely/unlikely的适用条件

C++20中的likely/unlikely


流水线技术

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

  • 取指:将指令从存储器中读取出来,放入指令缓冲区中。
  • 译码:对取出来的指令进行翻译
  • 执行:知晓了指令内容,便可使用CPU中对应的计算单元执行该指令
  • 访存:将数据从存储器读出,或写入存储器
  • 写回:将指令的执行结果写回到通用寄存器组

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

分支预测

我们都知道,程序的控制流程基本可分为三种:顺序、分支和循环。对CPU流水线来说,顺序比较好处理,一条路往前趟就行了。但是当程序中有了分支结构之后,CPU无法确切知道到底应该取分支1中的D指令,还是分支二中的E指令。此时CPU会根据指令执行的上下文,猜测那一路分支应该被执行。预测的结果有两个,命中或者命不中。在前一种情况下,CPU流水线正常执行,不会被打断。在后一种情况下,需要CPU丢掉为跳转指令之后的所有指令所做的工作,再开始从正确位置处起始的指令去填充流水线,这会导致很严重的惩罚:大约20-40个时钟周期的浪费,导致程序性能的严重下降。

什么是likely和unlikely

既然程序是我们程序员所写,在一些明确的场景下,我们应该比CPU和编译器更了解哪个分支条件更有可能被满足。我们是否可将这一先验知识告知编译器和CPU, 提高分支预测的准确率,从而减少CPU流水线分支预测错误带来的性能损失呢?答案是可以!它便是likelyunlikely。在Linux内核代码中,这两个宏的应用比比皆是。下面是他们的定义:

#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

上述源码中采用了内建函数__builtin_expect来进行定义,即 built in function。
  __builtin_expect的函数原型为long __builtin_expect (long exp, long c),返回值为完整表达式exp的值,它的作用是期望表达式exp的值等于c(如果exp == c条件成立的机会占绝大多数,那么性能将会得到提升,否则性能反而会下降)。注意, __builtin_expect (lexp, c)的返回值仍是exp值本身,并不会改变exp的值。
  __builtin_expect函数用来引导gcc进行条件分支预测。在一条指令执行时,由于流水线的作用,CPU可以同时完成下一条指令的取指,这样可以提高CPU的利用率。在执行条件分支指令时,CPU也会预取下一条执行,但是如果条件分支的结果为跳转到了其他指令,那CPU预取的下一条指令就没用了,这样就降低了流水线的效率。
  另外,跳转指令相对于顺序执行的指令会多消耗CPU时间,如果可以尽可能不执行跳转,也可以提高CPU性能。
  简单从表面上看if(likely(value)) == if(value),if(unlikely(value)) == if(value)。
也就是likely和unlikely是一样的,但是实际上执行是不同的,加likely的意思是value的值为真的可能性更大一些,那么执行if的机会大,而unlikely表示value的值为假的可能性大一些,执行else机会大一些。
  加上这种修饰,编译成二进制代码时likely使得if后面的执行语句紧跟着前面的程序,unlikely使得else后面的语句紧跟着前面的程序,这样就会被cache预读取,增加程序的执行速度。
  那么上述定义中为什么要使用!!符号呢?
  计算机中bool逻辑只有0和1,非0即是1,当likely(x)中参数不是逻辑值时,就可以使用!!符号转化为逻辑值1或0 。比如:!!(3)=!(!(3))=!0=1,这样就把参数3转化为逻辑1了。
  那么简单理解就是:
  likely(x)代表x是逻辑真(1)的可能性比较大;
  unlikely(x)代表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, 8test    edi, edi                jle     .L2                     ; 如果argc <= 0, 跳转到L2mov     edi, OFFSET FLAT:.LC0   ; 如果argc > 0, 从这里执行call    puts
.L3:xor     eax, eaxadd     rsp, 8ret
.L2:mov     edi, OFFSET FLAT:.LC1call    putsjmp     .L3

接着我们在if分支中加上unlikely, 反汇编如下。这里的情况正好与对照组相反,if分支下的指令被编译器放置于远离跳转指令jg的位置。这意味着CPU此时更倾向于走else分支。

.LC0:.string "Positive\n"
.LC1:.string "Zero or Negative\n"
main:sub     rsp, 8test    edi, edijg      .L6mov     edi, OFFSET FLAT:.LC1call    puts
.L3:xor     eax, eaxadd     rsp, 8ret
.L6:mov     edi, OFFSET FLAT:.LC0call    putsjmp     .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;}
}

#C++# #likely# #unlikely#减少CPU流水线分支预测错误带来的性能损失相关推荐

  1. 一步步编写操作系统 31 cpu的分支预测 下

    让我们说说预测的算法吧. 对于无条件跳转,没啥可犹豫的,直接跳过去就是了.所谓的预测是针对有条件跳转来说的,因为不知道条件成不成立.最简单的统计是根据上一次跳转的结果来预测本次,如果上一次跳转啦,这一 ...

  2. 一步步编写操作系统 30 cpu的分支预测简介

    人在道路的分岔口时要预测哪条路能够到达目的地,面对众多选择时,计算机也一样要抉择,毕竟计算机的运行方式是以人的思路来设计的,计算机中的抉择其实就是人在抉择. cpu中的指令是在流水线上执行.分支预测, ...

  3. Pentium 4处理器架构/微架构/流水线 (7) - NetBurst前端详解 - 分支预测

    Branch Predication 对于使用深度指令流水线的处理器,分支预测能力至关重要.分支预测使得处理器可以在分支指令决断之前就开始执行(预测的)分支路径指令.分支延迟是由于分支预测错误导致的性 ...

  4. if-else走天下,让CPU分支预测技术浮出水面

    关键字 圈复杂度 CPU分支预测机制 指令 吞吐量 IPS-每秒指令 GIPS-每秒十亿指令 延迟-皮秒 分支预测 if-else走天下 圈复杂度 void sort(int *A) { int i ...

  5. 阿里程序员工作小技巧:理解CPU分支预测,提高代码效率

    技术传播的价值,不仅仅体现在通过商业化产品和开源项目来缩短我们构建应用的路径,加速业务的上线速率,体现也会在优秀程序员在工作效率提升,产品性能优化和用户体验改善等小技巧方面的分享,以提高我们的工作能力 ...

  6. 阿里程序员工作小技巧 | 理解CPU分支预测,提高代码效率

    技术传播的价值,不仅仅体现在通过商业化产品和开源项目来缩短我们构建应用的路径,加速业务的上线速率,也会体现在优秀程序员在工作效率提升.产品性能优化和用户体验改善等小技巧方面的分享,以提高我们的工作能力 ...

  7. cpu 分支预测对性能的影响

    cpu 分支预测对性能的影响 现在的 cpu 一般都支持分支预测功能.维基百科中有以下描述: 在计算机体系结构中,分支预测器(英语:Branch predictor)是一种数字电路,在分支指令执行结束 ...

  8. Pentium II Pentium III架构/微架构/流水线 (2) - P6详解 - 前端(指令预取/译码/动态分支预测静态分支预测)

    Pentium II & III Instruction Pipeline Details Front-End Pipeline Details Pentium II & III处理器 ...

  9. 深入理解操作系统(12)第四章:处理器体系结构(4)Y86-64的流水线实现(包括:PIPE-处理器/预测下一个PC/分支预测/流水线冒险/暂停,转发避免冒险/PPE硬件结构及实现/CPI)

    深入理解操作系统(12)第四章:处理器体系结构(4)Y86-64的流水线实现(包括:PIPE-处理器/预测下一个PC/分支预测/流水线冒险/暂停,转发避免冒险/PPE硬件结构及实现/CPI) 1. Y ...

最新文章

  1. matlab plot画图指定线型和颜色
  2. IOS 其他 - 在真机调试的时候,将NSLog日志存入文件并保存到document目录
  3. SpringMVC中@GetMapping和@RequestMapping的区别
  4. np.random.rand()用法
  5. 如何使用Angular rxjs进行优雅限流
  6. java 得到checkbox_【JavaWeb】获得选中的checkbox的value
  7. 建筑工程计算机实验室简介,计算机实验室简介
  8. django解决:ModuleNotFoundError: No module named ‘django.core.urlresolvers‘
  9. JAVA 字符串驻留池
  10. python软件下载3版本-【python3下载】python3下载安装 中文版-七喜软件园
  11. java 数据输出详解_JAVA之I/O 输入输出流详解
  12. oracle11g密码效期及用户锁定
  13. rospy Odometry天坑小计
  14. 安装CAD缺少html,未安装.net无法安装cad2007怎么办
  15. highcharts去水印方法
  16. 【20CSPS提高组】函数调用
  17. 如何成为一名程序员?
  18. 我理解数字滤波器 -电容
  19. 3D打印机可以打印什么?
  20. Java 一元、二元运算符、三目条件运算符(三元运算符)

热门文章

  1. 一文读懂功率信号源(信号发生器)
  2. Day88.七牛云: 房源图片、用户头像上传 Common-upload、Webuploader
  3. ThingJS那些笔记
  4. 云主机vnc登录输入卡顿问题解决
  5. 【Python】爬取中国历史票房榜,可视化分析
  6. 100个python算法超详细讲解:黑白子交换
  7. android手机连接AS的adb wifi测试教程
  8. sequelize笔记
  9. Web前端 PS切图
  10. 多肽、instanceof