一.前言
1.編譯器優化介紹:
由于記憶體通路速度遠不及CPU處理速度,為提高機器整體性能,在硬體上引入硬體高速緩存Cache,加速對記憶體的通路。另外在現代CPU中指令的執行并不一定嚴格按照順序執行,沒有相關性的指令可以亂序執行,以充分利用CPU的指令流水線,提高執行速度。以上是硬體級别的優化。再看軟體一級的優化:一種是在編寫代碼時由程式員優化,另一種是由編譯器進行優化。編譯器優化常用的方法有:将記憶體變量緩存到寄存器;調整指令順序充分利用CPU指令流水線,常見的是重新排序讀寫指令。對正常記憶體進行優化的時候,這些優化是透明的,而且效率很好。由編譯器優化或者硬體重新排序引起的問題的解決辦法是在從硬體(或者其他處理器)的角度看必須以特定順序執行的操作之間設定記憶體屏障(memory barrier),linux 提供了一個宏解決編譯器的執行順序問題。
void Barrier(void)
這個函數通知編譯器插入一個記憶體屏障,但對硬體無效,編譯後的代碼會把目前CPU寄存器中的所有修改過的數值存入記憶體,需要這些資料的時候再重新從記憶體中讀出。
2.volatile總是與優化有關,編譯器有一種技術叫做資料流分析,分析程式中的變量在哪裡指派、在哪裡使用、在哪裡失效,分析結果可以用于常量合并,常量傳播等優化,進一步可以消除一些代碼。但有時這些優化不是程式所需要的,這時可以用volatile關鍵字禁止做這些優化。
二.volatile詳解:
1.volatile的本意是“易變的” 因為通路寄存器要比通路記憶體單元快的多,是以編譯器一般都會作減少存取記憶體的優化,但有可能會讀髒資料。當要求使用volatile聲明變量值的時候,系統總是重新從它所在的記憶體讀取資料,即使它前面的指令剛剛從該處讀取過資料。精确地說就是,遇到這個關鍵字聲明的變量,編譯器對通路該變量的代碼就不再進行優化,進而可以提供對特殊位址的穩定通路;如果不使用valatile,則編譯器将對所聲明的語句進行優化。(簡潔的說就是:volatile關鍵詞影響編譯器編譯的結果,用volatile聲明的變量表示該變量随時可能發生變化,與該變量有關的運算,不要進行編譯優化,以免出錯)
2.看兩個事例:
1>告訴compiler不能做任何優化
比如要往某一位址送兩指令:
int *ip =...; //裝置位址
*ip = 1; //第一個指令
*ip = 2; //第二個指令
以上程式compiler可能做優化而成:
int *ip = ...;
*ip = 2;
結果第一個指令丢失。如果用volatile, compiler就不允許做任何的優化,進而保證程式的原意:
volatile int *ip = ...;
*ip = 1;
*ip = 2;
即使你要compiler做優化,它也不會把兩次付值語句間化為一。它隻能做其它的優化。
2>用volatile定義的變量會在程式外被改變,每次都必須從記憶體中讀取,而不能重複使用放在cache或寄存器中的備份。
例如:
volatile char a;
a=0;
while(!a){
//do some things;
}
doother();
如果沒有 volatiledoother()不會被執行
3.下面是使用volatile變量的幾個場景:
1>中斷服務程式中修改的供其它程式檢測的變量需要加volatile;
例如:
static int i=0;
int main(void)
{
...
while (1){
if (i) dosomething();
}
}
void ISR_2(void)
{
i=1;
}
程式的本意是希望ISR_2中斷産生時,在main函數中調用dosomething函數,但是,由于編譯器判斷在main函數裡面沒有修改過i,是以可能隻執行一次對從i到某寄存器的讀操作,然後每次if判斷都隻使用這個寄存器裡面的“i副本”,導緻dosomething永遠也不會被調用。如果将變量加上volatile修飾,則編譯器保證對此變量的讀寫操作都不會被優化(肯定執行)。此例中i也應該如此說明。
2>多任務環境下各任務間共享的标志應該加volatile
3>存儲器映射的硬體寄存器通常也要加voliate,因為每次對它的讀寫都可能有不同意義。
例如:
假設要對一個裝置進行初始化,此裝置的某一個寄存器為0xff800000。
int *output = (unsigned int *)0xff800000;//定義一個IO端口;
int init(void)
{
int i;
for(i=0;i< 10;i++){
*output = i;
}
}
經過編譯器優化後,編譯器認為前面循環半天都是廢話,對最後的結果毫無影響,因為最終隻是将output這個指針指派為9,是以編譯器最後給你編譯編譯的代碼結果相當于:
int init(void)
{
*output = 9;
}
如果你對此外部裝置進行初始化的過程是必須是像上面代碼一樣順序的對其指派,顯然優化過程并不能達到目的。反之如果你不是對此端口反複寫操作,而是反複讀操作,其結果是一樣的,編譯器在優化後,也許你的代碼對此位址的讀操作隻做了一次。然而從代碼角度看是沒有任何問題的。這時候就該使用volatile通知編譯器這個變量是一個不穩定的,在遇到此變量時候不要優化。
例如:
volatile int *output=(volatile unsigned int *)0xff800000;//定義一個I/O端口
另外,以上這幾種情況經常還要同時考慮資料的完整性(互相關聯的幾個标志讀了一半被打斷了重寫),在1中可以通過關中斷來實作,2中禁止任務排程,3中則隻能依靠硬體的良好設計。
4.幾個問題
1)一個參數既可以是const還可以是volatile嗎?
可以的,例如隻讀的狀态寄存器。它是volatile因為它可能被意想不到地改變。它是const因為程式不應該試圖去修改它。
2) 一個指針可以是volatile 嗎?
可以,當一個中服務子程式修該一個指向一個buffer的指針時。
5.volatile的本質:
1> 編譯器的優化
在本次線程内, 當讀取一個變量時,為提高存取速度,編譯器優化時有時會先把變量讀取到一個寄存器中;以後,再取變量值時,就直接從寄存器中取值;當變量值在本線程裡改變時,會同時把變量的新值copy到該寄存器中,以便保持一緻。
當變量在因别的線程等而改變了值,該寄存器的值不會相應改變,進而造成應用程式讀取的值和實際的變量值不一緻。
當該寄存器在因别的線程等而改變了值,原變量的值不會改變,進而造成應用程式讀取的值和實際的變量值不一緻。
2>volatile應該解釋為“直接存取原始記憶體位址”比較合适,“易變的”這種解釋簡直有點誤導人。
6.下面的函數有什麼錯誤:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
該程式的目的是用來傳回指針*ptr指向值的平方,但是,由于*ptr指向一個volatile型參數,編譯器将産生類似下面的代碼:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地該變,是以a和b可能是不同的(如果沒有其他線程或則外部條件改變 *ptr,那麼這句話就沒錯!)。結果,這段代碼可能返不是你所期望的平方值!正确的代碼如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
注意:頻繁地使用volatile很可能會增加代碼尺寸和降低性能,是以要合理的使用volatile。
總結:
volatile變量有兩個作用:一個是告訴編譯器不要進行優化;
另一個是告訴系統始終從記憶體中取變量的位址,而不是從緩存或寄存器中取變量的值(加volatile和不加volatile系統都會産生緩存)