c語言volatile關鍵字的作用:提醒編譯器它後面所定義的變數隨時都有可能改變,因此編譯後的程式每次需要儲存或讀取這個變數的時候,告訴編譯器對該變數不做最佳化,都會直接從變數記憶體位址中讀取數據,從而可以提供對特殊位址的穩定訪問,以免出錯。
教學推薦:《c語言教學影片》
一.前言
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(); } } /* Interrupt service routine. */ 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可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr) { int a; a = *ptr; return a * a; }
注意:频繁地使用volatile很可能会增加代码尺寸和降低性能,因此要合理的使用volatile。
更多编程相关知识,请访问:编程教学!!
以上是c語言volatile關鍵字的作用是什麼?的詳細內容。更多資訊請關注PHP中文網其他相關文章!