【C/C++】volatile關鍵字

原文鏈接:https://www.cnblogs.com/zhao-zongsheng/archive/2018/05/26/9092520.html https://blog.csdn.net/wenqiang1208/article/details/71117818

編譯器對代碼的優化

本小節轉載自  作者:趙宗晟    出處:https://www.cnblogs.com/zhao-zongsheng/p/9092520.html

在講volatile關鍵字之前,先講一下編譯器的優化。

int main() {
    int i = 0;
    i++;
    cout << "hello world" << endl;
}

按照代碼,這個程序會在內存中預留int大小的空間,初始化這段內存爲0,然後這段內存中的數據加1,最後輸出“hello world”到標準輸出中。但是根據這段代碼編譯出來的程序(加-O2選項),不會預留int大小的內存空間,更不會對內存中的數字加1。他只會輸出“hello world”到標準輸出中。

其實不難理解,這個是編譯器爲了優化代碼,修改了程序的邏輯。實際上C++標準是允許寫出來的代碼和實際生成的程序不一致的。雖說優化代碼是件好事情,但是也不能讓編譯器任意修改程序邏輯,不然的話我們沒辦法寫可靠的程序了。所以C++對這種邏輯的改寫是有限制的,這個限制就是在編譯器修改邏輯後,程序對外界的IO依舊是不變的。怎麼理解呢?實際上我們可以把我們寫出來的程序看做是一個黑匣子,如果按照相同的順序輸入相同的輸入,他就每次都會以同樣的順序給出同樣的輸出。這裏的輸入輸出包括了標準輸入輸出、文件系統、網絡IO、甚至一些system call等等,所有程序外部的事物都包含在內。所以對於程序使用者來說,只要兩個黑匣子的輸入輸出是完全一致的,那麼這兩個黑匣子是一致的,所以編譯器可以在這個限制下任意改寫程序的邏輯。這個規則又叫as-if原則。不知道有沒有注意到,剛剛提到輸入輸出的時候,並沒有提到內存,事實上,程序對自己內存的操作不屬於外部的輸入輸出。這也是爲什麼在上述例子中,編譯器可以去除對i變量的操作。但是這又會出現一個麻煩,有些時候操作系統會把一些硬件映射到內存上,讓程序通過對內存的操作來操作這個硬件,比如說把磁盤空間映射到內存中。那麼對這部分內存的操作實際上就屬於對程序外部的輸入輸出了。對這部分內存的操作是不能隨便修改順序的,更不能忽略。這個時候volatile就可以派上用場了。

以下內容轉載自成長的菜鳥1018號的博客:https://blog.csdn.net/wenqiang1208/article/details/71117818

1、什麼是volatile

MSDN中對“volatile”的說明:

The volatile keyword is a type qualifier used to declare that an object can be modified in the program by something other than statements, such as the operating system, the hardware, or a concurrently executing thread.

volatile關鍵字是一種限定符用來聲明一個對象在程序中可以被語句外的東西修改,比如操作系統、硬件或併發執行線程。
遇到該關鍵字,編譯器不再對該變量的代碼進行優化,不再從寄存器中讀取變量的值,而是直接從它所在的內存中讀取值,即使它前面的指令剛剛從該處讀取過數據。而且讀取的數據立刻被保存。

下面寫個代碼測試一下volatile關鍵字
注:VC6.0中一般調試模式沒有進行代碼優化,所以這個關鍵字的作用看不出來。下面通過插入彙編代碼,測試有無 volatile 關鍵字,對程序最終代碼的影響:

#include <stdio.h>

    void main()
    {
        int i = 10;
        int a = i;

        printf("i = %d", a);

        // 下面彙編語句的作用就是改變內存中 i 的值
        // 但是又不讓編譯器知道
        __asm {
            mov dword ptr [ebp-4], 20h
        }

        int b = i;
        printf("i = %d", b);
    }

結果
在 Debug 版本模式運行程序:
i = 10 i = 32
在Release版本模式下運行程序:
i = 10 i = 10
上述 輸出的結果明顯表明,Release 模式下,編譯器對代碼進行了優化,第二次沒有輸出正確的 i 值。下面,我們把 i 的聲明加上 volatile 關鍵字(在這裏不再貼代碼,就是上面的代碼i前面加上volatile)

結果顯示:在Debug和Release版本模式下運行程序
i = 10 i =32
這就是表示volatile關鍵字發揮了作用

2、volatile在哪兒使用


一般說來,volatile用在如下的幾個地方:
(1)、中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
(2)、多任務環境下各任務間共享的標誌應該加volatile;
(3)、存儲器映射的硬件寄存器通常也要加volatile說明,因爲每次對它的讀寫都可能有不同意義;

另外,以上這幾種情況經常還要同時考慮數據的完整性(相互關聯的幾個標誌讀了一半被打斷了重寫),在1中可以通過關中斷來實現,2 中可以禁止任務調度,3中則只能依靠硬件的良好設計了。

3、多線程下的volatile


當兩個線程都要用到某一個變量且該變量的值會被改變時,應該用volatile聲明,該關鍵字的作用是防止優化編譯器把變量從內存裝入CPU寄存器中。如果變量被裝入寄存器,那麼兩個線程有可能一個使用內存中的變量,一個使用寄存器中的變量,這會造成程序的錯誤執行。volatile的意思是讓編譯器每次操作該變量時一定要從內存中真正取出,而不是使用已經存在寄存器中的值,如下:

volatile  BOOL  bStop  =  FALSE;  
  // (1) 在一個線程中:  
  while(  !bStop  )  {  ...  }  
  bStop  =  FALSE;  
  return;    
//(2) 在另外一個線程中,要終止上面的線程循環:  
  bStop  =  TRUE;  
  while(  bStop  );  //等待上面的線程終止,

如果bStop不使用volatile申明,那麼這個循環將是一個死循環,因爲bStop已經讀取到了寄存器中,寄存器中bStop的值永遠不會變成FALSE,加上volatile,程序在執行時,每次均從內存中讀出bStop的值,就不會死循環了。

4、volatile的特性


(1)易變性
所謂的易變性,在彙編層面反映出來,就是兩條語句,下一條語句不會直接使用上一條語句對應的volatile變量的寄存器內容,而是重新從內存中讀取。volatile的這個特性,相信也是大部分朋友所瞭解的特性。

(2)不可優化
測試非volatile變量

這裏寫圖片描述
測試volatile變量

這裏寫圖片描述

volatile告訴編譯器,不要對我這個變量進行各種激進的優化,甚至將變量直接消除,保證程序員寫在代碼中的指令,一定會被執行。

(3)順序性
C/C++ Volatile關鍵詞前面提到的兩個特性,讓Volatile經常被解讀爲一個爲多線程而生的關鍵詞:一個全局變量,會被多線程同時訪問/修改,那麼線程內部,就不能假設此變量的不變性,並且基於此假設,來做一些程序設計。當然,這樣的假設,本身並沒有什麼問題,多線程編程,併發訪問/修改的全局變量,通常都會建議加上Volatile關鍵詞修飾,來防止C/C++編譯器進行不必要的優化。但是,很多時候,C/C++ Volatile關鍵詞,在多線程環境下,會被賦予更多的功能,從而導致問題的出現。
以下測試在Linux 系統下,centos6.5
測試用例1:非volatile 變量
這裏寫圖片描述

注意:全局變量A,B均爲非volatile變量。通過gcc O2優化進行編譯,你可以驚奇的發現,A,B兩個變量的賦值順序被調換了!!!在對應的彙編代碼中,B = 0語句先被執行,然後纔是A = B + 1語句被執行。

在這裏,我先簡單的介紹一下C/C++編譯器最基本優化原理:保證一段程序的輸出,在優化前後無變化。將此原理應用到上面,可以發現,雖然gcc優化了A,B變量的賦值順序,但是foo()函數的執行結果,優化前後沒有發生任何變化,仍舊是A = 1;B = 0。因此這麼做是可行的。

測試用例2:一個volatile變量

這裏寫圖片描述

此測試,相對於測試用例五,最大的區別在於,變量B被聲明爲volatile變量。通過查看對應的彙編代碼,B仍舊被提前到A之前賦值,Volatile變量B,並未阻止編譯器優化的發生,編譯後仍舊發生了亂序現象。

如此看來,C/C++ Volatile變量,與非Volatile變量之間的操作,是可能被編譯器交換順序的。在多線程下,如此使用volatile,會產生很嚴重的問題。

測試用例3:兩個volatile變量

這裏寫圖片描述

同時將A,B兩個變量都聲明爲volatile變量,再來看看對應的彙編。奇蹟發生了,A,B賦值亂序的現象消失。此時的彙編代碼,與用戶代碼順序高度一直,先賦值變量A,然後賦值變量B。

如此看來,C/C++ Volatile變量間的操作,是不會被編譯器交換順序的。

下面看看在多線程下volatile的順序性

下面這段僞代碼,聲明另一個Volatile的flag變量。一個線程(Thread1)在完成一些操作後,會修改這個變量。而另外一個線程(Thread2),則不斷讀取這個flag變量,由於flag變量被聲明瞭volatile屬性,因此編譯器在編譯時,並不會每次都從寄存器中讀取此變量,同時也不會通過各種激進的優化,直接將if (flag == true)改寫爲if (false == true)。

這裏寫圖片描述

這隻要flag變量在Thread1中被修改,Thread2中就會讀取到這個變化,進入if條件判斷,然後進入if內部進行處理。在if條件的內部,由於flag == true,那麼假設Thread1中的something操作一定已經完成了,在基於這個假設的基礎上,繼續進行下面的other things操作。

但是實際情況中,不一定能保證flag == true時 ,something == 1,
在測試用例2中,C/C++ Volatile變量與非Volatile變量間的操作順序,有可能被編譯器交換。因此,上面多線程操作的僞代碼,在實際運行的過程中,就有可能變成下面的順序:

這裏寫圖片描述

所以當flag == true時 ,something == 1,
其實,針對這個多線程的應用,真正正確的做法,是構建一個happens-before語義。進入if語句中,先assert(somthing == 1),確保發生的情況下,執行otherthings。

小結:
C/C++ Volatile關鍵詞的第三個特性:”順序性”,能夠保證Volatile變量間的順序性,編譯器不會進行亂序優化。Volatile變量與非Volatile變量的順序,編譯器不保證順序,可能會進行亂序優化。同時,C/C++ Volatile關鍵詞,並不能用於構建happens-before語義,因此在進行多線程程序設計時,要小心使用volatile,不要掉入volatile變量的使用陷阱之中。

 

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