个人认为C++有着复杂、臃肿的语法系统,但是也正是因为这些特性,让我们在使用C++时既能深入到操作系统级的控制,也能抽象出来完全专注于一些业务问题。
这里为大家推荐一本书和汇编代码阅读网站!
《CPU眼里的C/C++》
Compiler Explorer
我们一起来抽丝剥茧,语法上的一切弯弯绕绕,在CPU汇编指令的面前众生平等。
文章目录
- 简述volatile概念
- 案例——不加valatile的情况
- 案例代码
- 案例——加上valatile关键字
- volatile的真正用意
- volatile的典型应用场景
- 1.多线程
- 不加volatile关键字
- 加上volatile关键字
- 2.驱动开发
- 不加volatile关键字
- 加上volatile关键字
- 总结
简述volatile概念
volatile可以放到变量的前面,告诉编译器这个变量是易变的、不稳定的。
只能说是一脸懵逼。
案例——不加valatile的情况
本节首先会给一个案例代码,其中包括C++源代码和对应的汇编指令。不懂汇编不要紧,我会进行详细的阐述说明。
首先我们一起探究一下对于一个常规变量,编译器是如何进行处理的。
案例代码
我们先从CPU的角度理解一下常规变量,然后做一下函数调用:左边CPP源代码的背景色和右边汇编的背景色是一一对应的。
我们对该案例进行以下分析:
- while循环体对应的的汇编指令
mov eax, DWORO PTR a[rip]
cmp eax, 1
jg .L2
指令一:读取变量a的值,将a的值(a是全局变量)读取到eax寄存器中。
指令二:比较a和1的大小
指令三:如果a大于1,则跳回,把前面两条指令再做一次。
- 提高编译器优化级别
我们将编译器的优化级别设置为-O2
,这通常针对编译过的程序进行性能优化的编译器选项,其目标是提升程序的执行速度,同时尽量保持编译时间和结果程序的大小在合理范围内。
我们整体的代码只上下五条汇编指令!因为编译器会把变量a当作常量来对待。既然a被认为是常量,那就说明a与1比较的结果对于编译器来说是预先可知的,所以我们的汇编竟然直接不执行while循环!
这就是编译器在背后为我们做的事情!
案例——加上valatile关键字
如果我们给a加上volatile
其他部分代码都不变,我们在a前面加上volatile关键字,汇编指令如下:
如上,尽管我们进行了2级优化,但是对于while对应的3条指令编译器并不会把它优化掉了!而是老老实实得从内存中取指令到寄存器,然后寄存器比较寄存器和1的大小,最后跳回或跳出循环。
volatile的真正用意
前文所说:“易变的、不稳定的”的描述其实都是说给编译器听的。
因为编译器会把他认为值不会改变的变量当作常量对待,以此缩减不必要的CPU指令,换取大幅度的效率优化。而volatile就是阻止这种优化,让CPU老老实实从内存中读、写变量。
由于编译器的技术进步和各大编译器之间的巨大差异,判定一个变量是否可以被优化,也没有一个统一的标准。这也就是volatile存在的意义!
volatile的典型应用场景
1.多线程
不加volatile关键字
我们在另外一个文件file_B或者另外一个线程task_B中,改变了a的值,让其满足while循环的条件。
//file_A.cpp
int a = 0;
int task_A() {
while(a > 1) {
// do something
}
return 1;
}
//file_B.cpp
extern int a;
void task_B() {
a = 2;
}
我们明明已经在文件B或者线程B的task_B函数更改了,a的值,但是由于编译器将while循环的指令进行了优化,while直接不满足条件而跳出循环!
加上volatile关键字
加上volatile关键字后,编译器不进行优化,CPU老老实实按照我们的想法来干活。
2.驱动开发
不加volatile关键字
在做驱动开发的时候,我需要通过读一个寄存器来了解USB设备的插拔状态:
unsigned int *REGISTER = (unsigned int *)0xFF001100;
int detect () {
//initialize register
*REGISTER = 0xff;
//read register for USB status
unsigned int status = *REGISTER;
return status;
}
这里经过编译器的代码优化后,尽管将REGISTER寄存器的值乖乖读到rax中,但是我们的unsigned int status = *REGISTER
竟然被直接优化了!
因为在编译器眼里,我们就是在读一个毫无变化的值,所以他干脆不读rax寄存器,而是直接把立即数255(0xff)返回。这样我们得到的USB状态永远都是毫无意义的255。
加上volatile关键字
加上之后,我们的编译器终于开始认认真真的读rax中的寄存器数字了。很舒服
总结
- 编译器的代码优化能力:编译器可能对代码中的变量读、写进行适当的优化,避免没有必要的内存读写操作,这往往会大幅度提升程序的执行效率。但当程序变得复杂时,就算编译器也不能完全领会程序员意图,所以这种优化有时候是有害的,需要volatile站出来解决。
- volatile关键字到底有什么用:volatile关键字,就是用来避免编译器的优化操作,用来保证每次对变量的读、写都是对内存的真实操作;特别是不会让编译器把某些变量当作常量对待。
- 如何判断开不开优化:在编译器不开优化的情况下,很多时候,是否加volatile不会有什么差异,这也让volatile的使用场景变得模糊。判定volatile是否有存在的必要,往往需要查看代码对应的CPU(汇编)指令,看看它是否符合程序员预期。
《CPU眼里的C/C++》的作者阿布大哥在本节最后讲了他的volatile应用场景:
“网卡会把每一个以太网数据包发送两遍。虽然依靠强大的TCP/IP协议的应对能力,这并不会影响网络通信和软件功能。
最后经过排查网卡驱动程序,之所以发送两次,是因为程序判定每次发送以太网包都是不成功的!所以会尝试在发一次。明明成功地发送了,为何被判定为不成功呢?
原来用来标识成功与否的寄存器,他的值被保存在一个变量里面,但由于优化的原因,而该变量被编译器当作常量0来对待了!”