補遺篇之volatile

    Cvolatile關鍵字在程序操作變量時,強制讀寫變量所在內存,以阻止編譯器對某些特殊變量的錯誤優化。反過來,只有靠程序員用volatile過濾一些特殊情況後,編譯器才能大膽優化。volatile作用可總結爲:阻止三種情形下的兩種編譯器優化

兩種編譯器優化

    a. 數據流分析優化:編譯器分析程序中變量在哪裏賦值、哪裏使用、哪裏失效,根據分析結果消除多餘的變量讀取和賦值步驟,如:

    int a = 10;

    ......//其他代碼,裏面沒有對a的讀操作

    a = 20;

    開啓了優化選項的編譯器能夠根據a賦值和使用情況,推斷a=10無效,直接忽略這條語句。

    b. 寄存器緩存技術:把頻繁訪問的變量緩存到某寄存器,之後就通過此寄存器訪問該變量,而不再通過內存總線。由於寄存器比內存快很多,這種智能優化可顯著提高性能。

三種不能優化的情形

    某些變量會在可見代碼外而不是被程序本身賦值改變,這時編譯器根據顯式代碼採取的上面兩種優化可能導致錯誤結果,這些特殊變量主要出現在以下三種情況:

    a. MMIOMemory Mapped IO)操作,某些CPU把外設I/O端口映射到內存空間統一編址,之後就象訪問普通內存那樣訪問MMIO,不需要專門I/O指令。MMIO內存的值對應於IO寄存器,會隨外部信號而改變,不完全依賴於代碼裏的顯式內存讀寫操作,很明顯這類內存就不能用上文兩種技術優化。這種volatile典型應用多出現在嵌入式驅動程序中。

    b. 變量被代碼內的內嵌彙編改變,而編譯器無法知道內嵌彙編裏變量的改變。

    c. 被中斷服務子程序訪問到的,或者在多線程應用中被幾個任務共享的全局變量。

這三種情況,必須用volatile阻止編譯器“想當然”的優化,下面舉例說明:

1:

    volatile int *p = get_io_addr();

    int a, b;

    a = *p;

    ......//其他代碼,裏面沒有對p的操作

    b = *p;

    p是指向MMIO的指針,例1中兩次讀取信號,賦給ab。如果p不聲明爲volatile,編譯器會自作聰明認爲兩次*p值一樣(普通內存的確如此,因爲中間沒賦值)b=*p時無需通過p指針讀取真實外設IO值,可用a=*p時保存在某寄存器的值代替。但外界信號可能隨時變化,一旦在a=*pb=*p間變化,這種用寄存器代替內存的優化就會出錯。

volatile同樣也用於阻止MMIO寫操作的優化,如果給變量賦值但後面沒使用,編譯器一般會忽略這次賦值操作,但MMIO賦值不同,因爲CPU通過MMIO設到硬件寄存器的數據,總有意義(如驅動LED/馬達等),必須用volatile以強制執行此類MMIO寫操作。

    volatile int *p = set_io_addr();   //對外輸出控制信號的IO寄存器映射的內存地址

    int j;                       //普通變量

   *p = 1; //不被優化 i=1

    *p = 3; //不被優化 i=3

    j = 1; //被優化掉

    j = 3; //j = 3

2

    void main()

    {

      int i=10;

      int a = i;

      printf("i1= %d\n",a);

      __asm {   mov dword ptr [ebp-4], 20h  }   //改變內存中i的值爲20h32,而優化器並不知道   

      int b = i;

      printf("i2= %d\n",b);

    }

    以上代碼在VC debug模式運行,輸出i1=10i2=32release模式輸出:i1 = 10i2 = 10。因爲release模式下編譯器默認開啓優化,在b=i時取之前a=i時讀到寄存器緩存值,而內嵌彙編操作不被編譯器注意。如果定義volatile int i=10,其他不變,debugrelease都輸出:i1=10i2=32。這說明volatile能阻止release模式下編譯器“自以爲是”的優化。

3

    多任務/中斷環境下,某線程中的變量可能被其他線程或中斷改變,而編譯器無從獲知這種改變,於是導致錯誤優化,如:

    int i=0;

    void main(void)

    {...

      while (1)

      {

        if (i) do_xxx();

      }

    }

    void ISR_XX(void)   /* Interrupt service routine. */

    {

       i=1;

    }

    程序本意希望中斷髮生時,main調用do_xxx函數,但編譯器不知道i會被ISR_XX修改,它通過本地代碼判斷main函數裏i從沒修改,因此只執行一次從內存i到寄存器的讀操作,之後每次if判斷都用寄存器裏i副本,而副本值永遠是初始值0,即使iISR_XX裏被改爲1do_xxx也不會被調用。強制從內存讀取i,就要定義volatile int i;

    不過即使用volatile避免了錯誤優化,也不能像上例那樣用volatile全局變量去同步線程。因爲volatile變量不滿足原子性和順序性,除非加鎖保護,而加鎖就不需要volatile了:鎖可以保證臨界區串行,也可以實現內存屏障(barrier),保證臨界區內的全局變量爲最新值而不是寄存器緩存,和volatile作用相同。關於volatile和線程同步超出範圍,不詳述。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章