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