1. volatile关键字可以让编译器层面减少优化,每次使用时必须从内存中取数据,而不是从cpu缓存或寄存器中获取
2. volatile关键字不能完全禁止编译器的指令重排,准确地说是两个volatile修饰的变量之间的命令不会进行指令重排
3. 使用volatile可以解决一部分的线程并发问题,但是不能解决所有的并发问题,该加锁时还要加锁才行
4. volatile关键字最多只能让编译器不做指令重排的优化,但是cpu层面的指令重排仍然不受影响。如果想禁止cpu层面的指令重排,可以使用__sync_synchronize()
实验代码如下:
const auto start = system_clock::now();
int i = 0;
while (i<50000000)
{
i++;
}
cout << duration_cast(system_clock::now() - start).count() << endl;
代码很简单,就是把变量i自增5000万次,最后计算这个过程用的毫秒数
debug模式下,打印结果如下:
release模式下,打印结果如下:
可以看到,同样的代码,
debug模式下耗时15毫秒
release模式下竟然耗时0毫秒,也就是程序根本就没执行5000万次自增,因为编译器发现5000万次自增后面压根没用到变量i,它认为这些操作毫无意义,然后自作主张地跳过了5000万次自增对应的编译命令,所以耗时0毫秒
实验代码如下:
int a = 0;
int b = 0;
void foo(void)
{
a = b + 1;
b = 2;
}
在debug模式下,我们在代码"b = 2"这一行,打上断点,并打开反汇编窗口查看对应的汇编指令,可以看到如下图
"a = b + 1" 对应3个汇编指令
"b = 2" 对应1个汇编指令
各个汇编指令完全符合我们的预期
改为release模式,我们打开反汇编查看对应更大汇编指令,如下图
会发现
第二句代码"b = 2"对应的汇编指令居然在前面
第一句代码"a = b+1"对应的汇编指令反而在后面,且经过了变形
也就是说,编译器帮我们进行了指令优化和重排
我们给变量a加上volatile关键字,代码如下
volatile int a = 0;
int b = 0;
void foo(void)
{
a = b + 1;
b = 2;
}
release模式下,再次查看反汇编,如下图
可以看到,给变量a使用volatile关键字修饰后,编译器虽然强制从内存取了一次数据,但仍然进行了指令优化,比如直接把1赋给了变量a,然后才对变量b的寄存器执行xor操作,仍然不是最初的汇编指令。
我们给变量a,变量b都加上volatile,代码如下
volatile int a = 0;
volatile int b = 0;
void foo(void)
{
a = b + 1;
b = 2;
}
在release模式下,再查看反汇编窗口,如下图,总算符合我们的预期了
可以看到,现在总算正常了,几乎跟debug版的汇编指令一模一样
代码"a = b + 1",对应了4条汇编指令
代码"b = 2",对应了1条汇编指令
结合本例和上面的优化一,证明了:
单个volatile变量并不能完全阻止编译器对指令进行重排优化,两个volataile共同作用才能阻止编译器对两个volatile变量之间的汇编指令进行重排优化
注意:虽然两个volatile变量可以限制编译器层面的指令重排,但是cpu还是有可能出现指令重排,因为cpu为了提高执行速度,不会严格按照编译后的汇编指令进行执行,它同样会进行一定的优化,以至于最终结果和我们设想的不一致,请参考我的这篇博客
cpu的指令重排,禁用办法-CSDN博客