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指令與之對應。
由于編譯器知道這條語句之后內存發生了變化,編譯器在編譯時就會保證這條語句上下的指令不會亂,即這條語句上面的指令,不會亂序到語句下面,語句下面的指令不會亂序到語句上面。
利用編譯器這個功能,程序員可以:
1、利用這條語句,強制程序訪存,而不是使用寄存器中的值,作為使用volatile關鍵字的一個替代手段;
2、在不允許亂序的兩個語句之間插入這條語句從而保證不會被編譯器亂序。
???
下面看一個應用的例子,兩個線程訪問共享的全局變量:
#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指令亂序的話題,感興趣的可以參考以下文章了解以下:
Memory Reordering Caught in the Act This Is Why They Call It a Weakly-Ordered CPUvolatile關鍵字的使用
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/ 轉載請注明出處
????
轉載于:https://www.cnblogs.com/jintianfree/p/4375934.html
總結
以上是生活随笔為你收集整理的volatile关键字及编译器指令乱序总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Go语言]从Docker源码学习Go—
- 下一篇: Qt 智能指针学习