volatile關鍵字及編譯器指令亂序總結

本文簡單介紹volatile關鍵字的使用,進而引出編譯期間內存亂序的問題,並介紹了有效防止編譯器內存亂序所帶來的問題的解決方法,文中簡單提了下CPU指令亂序的現象,但並無深刻討論。     程序員

如下是我搭建的博客地址: http://itblogs.ga/blog/20150329150706/    歡迎到這裏閱讀文章。編程

volatile關鍵字

volatile關鍵字用來修飾一個變量,提示編譯器這個變量的值隨時會改變。一般會在多線程、信號處理、中斷處理、讀取硬件寄存器等場合使用。數組

程序在執行時,一般將數據(變量的值)從內存的讀到寄存器中,而後進行運算,此後對該變量的處理,都是直接訪問寄存器就能夠了,再也不訪問內存,由於 訪存的代價是很高的(這塊是訪問寄存器仍是從新訪存加載到寄存器是編譯器在編譯階段就決定了的)。但在上述說的幾種狀況下,內存會被另外一個線程或者信號處 理函數、中斷處理函數、硬件改掉,這樣,代碼只訪問寄存器的話,永遠得不到真實的值。緩存

   

對這樣的變量(會在多線程、線程與信號、線程與中斷處理中共同訪問的,或者硬件寄存器),在定義時都會加上volatile關鍵字修飾。這樣編譯器 在編譯時,編譯出的指令會從新訪存,這樣就能保證拿到正確的數據了。但這裏須要注意的是,編譯器只能作到讓指令從新訪問內存,而不是直接使用寄存器中的 值,這些和緩存沒有關係,具體執行時指令是訪問內存仍是訪問的緩存,編譯器也沒法干預。多線程

   

另外,除了使用寄存器來避免屢次訪存外,編譯器有時可能直接將變量所有優化掉,使用常數代替。好比:架構

int main()
{
    int a = 1;
    int b = 2;

    printf("a = %d, b = %d \n", a, b);
}函數

   

編譯器可能直接優化爲:      性能

int main()
{
    printf("a = %d, b = %d \n", 1, 2);
}優化

   

  若是對ab的聲明加了 volatile關鍵字,編譯器將不在作這樣的優化。線程

             

還有,對全部volatile變量,編譯器在編譯階段保證不會將訪問volatile變量的指令進行亂序重排。

    

   

  指令亂序

那麼什麼是指令亂序,指令亂序是爲了提升性能,而致使的執行時的指令順序和代碼寫的順序不一致。指令亂序有編譯期間指令亂序和執行時指令亂序。

執行時指令亂序是CPU的一個特性,這塊比較複雜,再也不這裏說起。咱們只須要知道在x86/x64的體系架構下,程序員通常不須要關注執行時指令亂序(不須要關注不表明沒有)。

編譯期間指令亂序是指在編譯成二進制代碼時,編譯器爲了所謂的優化進行了指令重排,致使二進制指令的順序和咱們寫的代碼的順序是不一致的。

好比如下代碼:

int a;
int b;

int main()
{
    a = b + 1;
    b = 0;
}

會被優化成(實際上在彙編階段進行的亂序優化,優化後的代碼也只能以彙編的方式查看,這裏只是拿C代碼舉例說明一下):

int a;
int b;

int main()
{
    b = 0;
    a = b + 1;
}

對加上volatile關鍵字的變量的訪問,編譯器不會進行指令亂序的優化,保證volatile變量的訪問順序和代碼寫的是同樣的。好比以下代碼不會優化:

volatile int a;
volatile int b;

int main()
{
    a = b + 1;
    b = 0;
}

   

可是如下代碼,依然會亂序,由於編譯器只是保證volatile變量訪問的順序,對於非volatile變量之間,以及volatile以及非volatile變量之間的順序,編譯器仍是會優化。

int a;
volatile int b;

int main()
{
    a = b + 1;
    b = 0;
}

   

       

asm volatile ("" : : : "memory");

通常編程時若是使用到volatile關鍵字,那麼基本上都須要考慮編譯器指令亂序的問題。解決編譯器指令亂序所帶來的問題,除了上面將必要的變量聲明爲volatile,還可使用下面一條嵌入式彙編語句:

1 asm volatile ("" : : : "memory");

這是一條空彙編語句,只是告訴編譯器,內存發生了變化。編譯器遇到這條語句後,會生成訪存更新寄存器的指令,將全部的寄存器的值更新一遍。這裏是編譯器遇到這條語句額外生成了一些代碼,而不是CPU遇到這條語句執行了一些處理,由於這條語句自己並無CPU指令與之對應。

因爲編譯器知道這條語句以後內存發生了變化,編譯器在編譯時就會保證這條語句上下的指令不會亂,即這條語句上面的指令,不會亂序到語句下面,語句下面的指令不會亂序到語句上面。

利用編譯器這個功能,程序員能夠:

一、利用這條語句,強制程序訪存,而不是使用寄存器中的值,做爲使用volatile關鍵字的一個替代手段;

二、在不容許亂序的兩個語句之間插入這條語句從而保證不會被編譯器亂序。

   

下面看一個應用的例子,兩個線程訪問共享的全局變量:

#define ARRAY_LEN 12

volatile int flag = 0;
int a[ARRAY_LEN];

pthread1()
{
    a[ARRAY_LEN - 1] = 10; <br>    asm volatile ("" : : : "memory");
    flag = 1;
}

pthread2()
{
    int sum = 0;

    if(flag == 0) {
        sum += a[ARRAY_LEN - 1];
    }    
}線程2假定flag==1時,線程1已經將數據放到數組中了。但實際上,若是沒有  asm volatile ("" : : : "memory"),線程1並不能保證flag = 1在數組賦值以後。緣由就是咱們前面提到的編譯器指令亂序。

     

指令亂序是一個比較複雜的話題,咱們這裏只考慮了編譯器指令亂序,在intel架構的CPU上,基本上考慮到這些就足夠了。但在弱指令序的CPU上,好比mips,瞭解這些還遠遠不夠。本文不打算展開CPU指令亂序的話題,感興趣的能夠參考如下文章瞭解如下:

   
   

volatile關鍵字的使用

volatile關鍵字使用和const一致,下面是一個總結:

char const * pContent;       // *pContent是const,   pContent可變
(char *) const pContent;     //  pContent是const,  *pContent可變
char* const pContent;        //  pContent是const,  *pContent可變
char const* const pContent;  //  pContent 和       *pContent都是const

   

沿着*號劃一條線,若是const位於*的左側,則const就是用來修飾指針所指向的變量,即指針指向爲常量;若是const位於*的右側,const就是修飾指針自己,即指針自己是常量。

   

   

參考資料

Memory Ordering at Compile Time

如下是我搭建的博客地址:原文連接:http://itblogs.ga/blog/20150329150706/ 轉載請註明出處
相關文章
相關標籤/搜索