volatile
的定義
volatile
告訴編譯器,變量的值可能會在編譯器不知道的情況下發生變化。因此,編譯器不能僅僅因為C程序似乎沒有更改值就不能假定值沒有更改。
另一方面,這意味著可能需要在編譯器不知道的其他地方讀取(讀取)變量的值,因此必須確保對變量的每個賦值實際上都是作為寫操作執行的。 / p>
用例
時需要
volatile
- 將硬件寄存器(或內存映射的I / O)表示為變量-即使永遠不會讀取寄存器,編譯器也不能只是跳過寫操作,認為“愚蠢的程序員。試圖將值存儲在變量中,他/她將永遠不會回讀。他/她甚至不會注意到我們是否忽略了文字。”相反,即使程序從不向變量寫入值,硬件也可能會更改其值。
- 在執行上下文之間共享變量(例如ISR /主程序)(請參閱@kkramo的答案)
volatile
的效果
在將變量聲明為 volatile
時,編譯器必須確保在程序代碼中對其進行的每個賦值均反映在實際的寫操作中,並且每次在程序代碼中進行的讀取均從(映射) )的內存。
對於非易失性變量,編譯器假定它知道是否/何時改變了變量的值,並可以以不同的方式優化代碼。
其中之一是,編譯器可以通過將值保留在CPU寄存器中來減少對內存的讀取/寫入次數。
示例:
void uint8_t計算(uint8_t輸入){
uint8_t結果=輸入+ 2;
結果=結果* 2;
如果(結果> 100){
結果-= 100;
}
返回結果;
}
在這裡,編譯器可能甚至不會為 result
變量分配RAM,並且永遠不會將中間值存儲在任何位置,而是存儲在CPU寄存器中。
如果 result
是易失性的,則C代碼中每次出現 result
都將要求編譯器執行對RAM(或I / O端口)的訪問,從而導致降低性能。
第二,編譯器可能會針對性能和/或代碼大小對非易失性變量進行重新排序。簡單的例子:
int a = 99;
int b = 1;
整數c = 99;
可以重新排序為
int a = 99;
整數c = 99;
int b = 1;
這可以節省彙編程序指令,因為值 99
不必兩次加載。
如果 a
, b
和 c
易失,則編譯器將必鬚髮出指令,以正確的順序分配值在程序中給出。
另一個經典示例如下:
volatile uint8_t信號;
void waitForSignal(){
而(信號== 0){
// 沒做什麼。
}
}
如果在這種情況下 signal
不是 volatile
,則編譯器會“認為” while(signal == 0)
可能是一個無限循環(因為 signal
永遠不會在循環內被代碼更改),並且可能會生成等價於
void waitForSignal(){
如果(信號!= 0){
返回;
}其他{
while(true){// <--無止境的循環!
// 沒做什麼。
}
}
}
考慮處理 volatile
值
如上所述,當 volatile
變量被訪問的次數比實際需要的次數多時,可能會導致性能下降。為緩解此問題,您可以通過分配給非易失性變量(如
volatile uint32_t sysTickCount;
void doSysTick(){
uint32_t ticks = sysTickCount; //對sysTickCount的單次讀取訪問
刻度=刻度+ 1;
setLEDState(打勾< 500000L);
if(tick > = 1000000L){
刻度= 0;
}
sysTickCount =滴答聲; //對易失sysTickCount的單個寫訪問
}
這可能在ISR中特別有用,在ISR中,您希望盡可能快地在您知道不需要使用相同的硬件或內存時不會多次訪問同一硬件或內存,因為在您運行ISR正在運行。當ISR是變量值的“生產者”時,例如在上例中的 sysTickCount
中,這是很常見的。在AVR上,讓函數 doSysTick()
訪問內存中相同的四個字節(四個指令=每次訪問 sysTickCount
的8個CPU週期)會特別痛苦。六次而不是兩次,因為程序員確實知道在他/她的 doSysTick()
運行時,其他代碼不會更改該值。
使用此技巧,您基本上可以對非易失性變量執行與編譯器相同的操作,即僅在必須時才從內存中讀取它們,將值保留在寄存器中一段時間,然後僅在發生時才寫回內存它必須但是這一次,您比(如果)讀/寫必須的時候要了解編譯器,因此您可以使編譯器從此優化任務中解脫出來,自己動手。
volatile
的限制
非原子訪問
volatile
是否not提供對多字變量的原子訪問。對於這些情況,除了使用 volatile
之外,您還需要通過 其他方式提供互斥。在AVR上,您可以使用 <util / atomic.h>
或簡單的 cli();中的 ATOMIC_BLOCK
。 ... sei();
調用。各個宏也充當內存屏障,這對於訪問順序很重要:
執行順序
volatile
僅對其他volatile變量施加嚴格的執行順序。這意味著,例如
volatile int i;
volatile int j;
詮釋
...
i = 1;
a = 99;
j = 2;
保證
首先 將1分配給 i
,然後然後將2分配給 j
。但是,不能保證在它們之間分配 a
;編譯器可以在代碼段之前或之後進行分配,基本上可以在任何時候進行,直到 a
的第一次(可見)讀取。
如果不是為了上述宏的存儲障礙,則允許編譯器進行翻譯
uint32_t x;
cli();
x = volatileVar;
sei();
到
x = volatileVar;
cli();
sei();
或
cli();
sei();
x = volatileVar;
(為了完整起見,我必須說,像sei / cli宏所隱含的那樣,內存障礙實際上可以消除對 volatile
的使用,如果 all 這些障礙被括在括號中。)