根据C语言标准,volatile关键字的作用是禁止编译器对相关变量的存取进行优化。本文利用VC 2010和GCC 4.4.7,分析volatile关键字对生成的汇编代码的影响,以验证volatile的具体含义。
VC 2010
以下是基础C代码
int gMark = 1;以下是VC 2010 Releae编译后的汇编代码,可以看到循环直接被被优化掉了。
int _tmain(int argc, _TCHAR* argv[])
{
while(gMark){
}
return 0;
}
00A51000 jmp wmain (0A51000h)
如果把gMark声明为volatile int,再次VC 2010 Releae编译,生成的汇编代码如下,可以看到每次循环都要重新读取一次gMark所在的内存。
01071000 mov eax,dword ptr [gMark(1073018h)]
01071005 test eax,eax
01071007 jne wmain (1071000h)
GCC 4.4.7
以下是基础C代码
int gMark = 1;以下是g++ -O3编译后的反汇编代码,可以看到循环直接被被优化掉了,与VC完全一样。
int main()
{
while(gMark){
}
return 0;
}
Dump of assembler code forfunction main:
0x0000000000400680 <+0>: jmp 0x400680 <main>
Endof assembler dump.
如果把gMark声明为volatile int,g++ -O3编译后的汇编代码如下。可以看到每次循环都要重新读取一次gMark所在的内存,这点与VC完全一致。
Dumpof assembler code for function main:
0x0000000000400680 <+0>: movl $0x1,-0x4(%rsp)
0x0000000000400688 <+8>: nopl 0x0(%rax,%rax,1)
0x0000000000400690 <+16>: mov -0x4(%rsp),%eax
0x0000000000400694 <+20>: test %eax,%eax
0x0000000000400696 <+22>: jne 0x400690 <main+16>
0x0000000000400698 <+24>: repz retq
Endof assembler dump.
结论
如果不加volatile关键字,编译器对C代码中的多次读取该变量的操作,有可能只生成一次加载内存到寄存器的指令。如果加上volatile,则每次C源码中用到该变量,都需要从内存中重新加载。
其它
以上基本说明了volatile的含义。另外,如果C代码的函数中有多处对volatile变量的读写,编译器还必须在生成的汇编代码中保留这些volatile变量的读写顺序,见wiki(http://en.wikipedia.org/wiki/Memory_barrier)。
还有一个经常混淆的问题是,volatile是否还定义了原子语义?用volatile修饰变量,能不能保证对一个变量的读写操作不会被其他线程打断?我们说根据C标准,volatile是没有原子语义的。另外,刚才生成的汇编代码也说明了volatile变量的读写是没有原子性的。
最后是volatile和memory barrier到底是什么关系。我的理解是,volatile是对编译器优化进行干预,memory barrier是对CPU的乱序执行进行干预,因此两者没有关系。但是有些编译器对volatile变量赋予了acquire和release语义,从而使得volatile能有一定的memory barrier的作用。详细见http://msdn.microsoft.com/zh-cn/library/ms686355。因为这和编译器以及编译器的版本是密切相关的,对于开发应用程序的程序员,就当它不存在好了。