根據C語言標準,volatile關鍵字的作用是禁止編譯器對相關變量的存取進行優化。本文利用VC 2010和GCC 4.4.7,分析volatile關鍵字對生成的彙編代碼的影響,以驗證volatile的具體含義。
VC 2010
以下是基礎C代碼
int gMark = 1;
int _tmain(int argc, _TCHAR* argv[])
{
while(gMark){
}
return 0;
}
以下是VC 2010 Releae編譯後的彙編代碼,可以看到循環直接被被優化掉了。
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;
int main()
{
while(gMark){
}
return 0;
}
以下是g++ -O3編譯後的反彙編代碼,可以看到循環直接被被優化掉了,與VC完全一樣。
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。因爲這和編譯器以及編譯器的版本是密切相關的,對於開發應用程序的程序員,就當它不存在好了。