本文轉(zhuǎn)自徐飛翔的“一文理解C語言中的volatile修飾符”
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。
前言
volatile
修飾符是在嵌入式開發(fā)和多線程并發(fā)編程中常見的修飾符,理解其對于實踐過程非常有幫助,此文參考了[1],并且附上了筆者的一些例子,希望對大家有所幫助。
volatile
修飾符用于C語言和C++中,其意在阻止編譯器對其修飾對象進(jìn)行任何形式的優(yōu)化,有時候,這種編譯器“自作主張”的優(yōu)化會導(dǎo)致編程者意想不到的結(jié)果,因此需要引入這個關(guān)鍵字進(jìn)行限制。
當(dāng)一個對象可能會被當(dāng)前代碼以外的環(huán)境,在任何時刻被改變的時候,一個對象如果此時被聲明為了volatile
,那么其就可以脫離編譯器的優(yōu)化過程。當(dāng)需要讀取該數(shù)據(jù)的時候,系統(tǒng)總是會重新從內(nèi)存位置中讀取當(dāng)前的volatile
類型的數(shù)據(jù),而不是直接取其在寄存器中的值,我們要知道,為了執(zhí)行效率,即便是你指定了要從之前的同一個對象中取值,編譯器在優(yōu)化過程中也很可能會不直接從內(nèi)存中讀取數(shù)據(jù),而是直接采用寄存器中的值(我們將會從后續(xù)的例子中看到這個情況。),這個行為在一般情況下的確能夠提高程序的執(zhí)行速率,畢竟數(shù)據(jù)從寄存器中讀取,要比從內(nèi)存中讀取快上好幾個數(shù)量級。比如我們見一個簡單例子:
// sample.c
int main(){
int i = 10;
i = i;
return 0;
}
在linux下用命令gcc -S sample.c
編譯,我們得到了其匯編結(jié)果,我截取了其主體,如:
movl $10, -4(%rbp)
而在sample.c
的變量聲明中如果加上volatile
修飾符,那么程序變成:
// sample.c
int main(){
volatile int i = 10;
i = i;
return 0;
}
匯編后的結(jié)果為:
movl $10, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, -4(%rbp)
我們對比這兩次的匯編結(jié)果,我們發(fā)現(xiàn),第一次沒有聲明其為易變的時候,編譯器分析了代碼的變量的關(guān)系,并且進(jìn)行了優(yōu)化,編譯器認(rèn)為,我的變量i
既然在下一步還需要賦值給自己,那么何必在重新從內(nèi)存中讀取i
的值呢,因此,從匯編來看,i = i
這條c語言代碼其實是無效的。 在一般的編譯器優(yōu)化中,因為編譯器可能會認(rèn)為變量i
是非易變的,如果其變化了,只能是因為程序員對其進(jìn)行了顯式的賦值改變,因此在需要再次讀取變量i
的值的時候,與其重新從內(nèi)存中讀取,不如直接利用其已經(jīng)讀入到寄存器中的值,畢竟寄存器比內(nèi)存快得多。
但是我們要思考下,i = i;
是不是沒有意義的代碼呢?我們很容易認(rèn)為這個答案 是的確沒有意義。
但是,我們假設(shè)有一種情況,在int i = 10;
之后,因為某種原因,比如硬件中斷,多線程的修改或者其他原因,導(dǎo)致此時i
改變了,而不是初始的i = 10
了,那么我們后續(xù)的代碼i = i;
就變得非常重要,因為其需要讀取在內(nèi)存中,新的值i
,而不是簡單的將其忽視掉或者簡單地讀取內(nèi)存中的值,注意到這個時候寄存器中的值已經(jīng)是“過時”了的,如果任由編譯器去優(yōu)化,那么你將永遠(yuǎn)無法讀取傳感器的值(傳感器的值很多由硬件中斷讀取。)
通過上面的討論,我們便能理解這兩個不同的匯編結(jié)果了,在第二段匯編中,我們不僅通過movl $10, -4(%rbp)
將直接數(shù)10
傳輸?shù)搅藘?nèi)存-4(%rbp)
(指的是寄存器%rbp
中的地址所指向的內(nèi)存偏移4個字節(jié)的內(nèi)存位置,是相對尋址的指令),而且接下來還重新讀取了該內(nèi)存位置的值,并且將其賦給了自己的這個內(nèi)存位置(這個過程中,因為該變量可能是易變的,因此該內(nèi)存可能會被其他程序給覆蓋,因此要重新讀?。?/p>
重新回到我們的討論,那么什么時候我們需要用volatile
這個修飾符呢?當(dāng)屬于下面幾種情況的時候,應(yīng)該考慮這個修飾符:
- 當(dāng)全局變量會被中斷服務(wù)函數(shù)給修改的時候。例如一個全局變量可以表示一個外部數(shù)據(jù)接口(通常全局指針被引用為內(nèi)存映射IO),這意味著該數(shù)據(jù)會被動態(tài)地更新。如果我們的代碼期望讀取數(shù)據(jù)接口的值,那么我們就應(yīng)該將其定義為
volatile
,以獲取其數(shù)據(jù)的最新值。如果我們不這么做,編譯器的優(yōu)化過程會使得只讀取一次該接口的數(shù)據(jù),并且將其加載到寄存器中,接下來都只能讀取該寄存器中的舊值了。 - 在多線程應(yīng)用中的全局變量。在多線程通信中,有著多種通信方式:信號傳遞(message passing),郵箱(mail boxes),共享內(nèi)存(shared memory)等。一個全局變量是共享內(nèi)存的樸素形式。當(dāng)兩個線程通過全局變量共享信息時,他們需要用
volatile
進(jìn)行修飾。因為線程是異步運行的,每個線程導(dǎo)致的全局變量的每次更新,都應(yīng)該被其他線程重新從內(nèi)存中獲取。為了消除編譯器優(yōu)化導(dǎo)致的效果,這些全局變量必須要用volatile
修飾。
如果我們不用volatile
修飾,有可能會導(dǎo)致以下問題:
- 當(dāng)編譯器優(yōu)化開啟時,代碼可能不會正常工作。
- 當(dāng)中斷發(fā)生時,代碼可能不會正常工作。
Reference
[1]. https://www.geeksforgeeks.org/understanding-volatile-qualifier-in-c/