《C語言 - 關鍵字volatile的作用》


前言:

編譯器優化介紹:

 由於內存訪問速度遠不及CPU處理速度,為提高機器整體性能,在硬件上引入硬件高速緩存Cache,加速對內存的訪問。另外在現代CPU中指令的執行並不一定嚴格按照順序執行,沒有相關性的指令可以亂序執行,以充分利用CPU的指令流水線,提高執行速度。以上是硬件級別的優化。

  軟件一級的優化:一種是在編寫代碼時由程序員優化,另一種是由編譯器進行優化。編譯器優化常用的方法有:將內存變量緩存到寄存器;調整指令順序充分利用CPU指令流水線,常見的是重新排序讀寫指令。

  編譯器有一種技術叫做數據流分析,分析程序中的變量在哪里賦值、在哪里使用、在哪里失效,分析結果可以用於常量合並,常量傳播等優化,進一步可以消除一些代碼。但有時這些優化不是程序所需要的,這時可以用volatile關鍵字禁止做這些優化。

 

1.volatile的作用

  告訴編譯器該變量是很容易被改變的。不要對該變量進行優化,即每次使用該變量時,系統總是重新從變量所在的內存讀取數據,即使它前面的指令剛剛從該處讀取過數據。而不是使用保存在寄存器中的備份。

  volatile的幾個例子:

  1.並行設備的硬件寄存器(如:狀態寄存器)

  2.一個中斷服務子程序中會訪問到的非自動變量(Non-automatic variables) 

  3.多線程應用中被幾個任務共享的變量 

事例:

告訴編譯器不要做優化:

比如要往某一地址送兩指令:
int *ip =...; //設備地址
*ip = 1; //第一個指令
*ip = 2; //第二個指令
以上程序compiler可能做優化而成:
int *ip = ...;
*ip = 2;
結果第一個指令丟失。如果用volatile, compiler就不允許做任何的優化,從而保證程序的原意:
volatile int *ip = ...;
*ip = 1;
*ip = 2;

 

用volatile定義的變量會在程序外被改變,每次都必須從內存中讀取,而不能重復使用放在cache或寄存器中的備份

volatile char a;
a=0;

while(!a){
    ;
}
doother();

  如果沒有 volatile,doother()不會被執行。

 

2.volatile的應用場景

1.中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
2.多任務環境下各任務間共享的標志應該加volatile;
3.存儲器映射的硬件寄存器通常也要加voliate,因為每次對它的讀寫都可能有不同意義。

 實例:

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的值,而在main函數中的i沒有被編譯器認為是1。而如果不是通過中斷修改i,而只是在程序中修改i的值,main函數中的i就會被認為是1。

  其實在中斷中使用全局變量也可以,但是你要確保該全局變量在程序中沒有被占用。

  就比如我在盯着一個房間有沒有人,我一開始進這個房間看了一眼,沒有人,那么這個時候我就只需要在門口盯着,確保沒有人從門口進去就行了。這個時候中斷來了,就類似時間被暫停了,然后有個人進房間了。然后時間恢復,這個時候我依舊只會盯着門看,不會進門去看一下,確認房間是否有人。因為我認為我一直在盯着門。

  這個就類似一開始編譯器從內存中拿出i的值放到寄存器中,然后if(i)反復判斷。編譯器就會認為我中間沒有去做其他事,沒有必要再去從內存中拿一次i的值。

  而如果再程序中修改i的值,就類似我在看着門,突然有事離開。然后再回來,我要確保我出去的這段時間,有沒有人進房間,所以會進房間看一次有沒有人。就類似從內存中取一次i的值。

 

假設要對一個設備進行初始化,此設備的某一個寄存器為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通知編譯器這個變量是一個不穩定的,在遇到此變量時候不要優化。

3..關於volatile的擴展

 一個參數既可以是const還可以是volatile嗎?

   是的。一個例子是只讀的狀態寄存器。它是volatile因為它可能被意想不到地改變。它是const因為程序不應該試圖去修改它。

 

一個指針可以是volatile 嗎?

  是的。一個例子是當一個中服務子程序修改一個指向一個buffer的指針時。

 

下面的函數有什么錯誤?

int square(volatile int *ptr) 
{ 
    return *ptr * *ptr; 
} 

  有可能前半段的*ptr和后半段的*ptr的值不一樣。應該修改成

long square(volatile int *ptr) 
{ 
    int a; 
    a = *ptr; 
    return a * a; 
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM