轉自:http://blog.csdn.net/beiyetengqing/article/details/49580559
1、首先爲何要指令重排序(instruction reordering)?
編譯器或運行時環境爲了優化程序性能而採取的對指令進行重新排序執行的一種手段。
2、是不是所有的語句的執行順序都可以重排呢?
2.1、什麼是數據依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個爲寫操作,此時這兩個操作之間就存在數據依賴。數據依賴分下列三種類型:
名稱 | 代碼示例 | 說明 |
寫後讀 | a = 1;b = a; | 寫一個變量之後,再讀這個位置。 |
寫後寫 | a = 1;a = 2; | 寫一個變量之後,再寫這個變量。 |
讀後寫 | a = b;b = 1; | 讀一個變量之後,再寫這個變量。 |
上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果將會被改變。所以,編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操作的執行順序。也就是說:在單線程環境下,指令執行的最終效果應當與其在順序執行下的效果一致,否則這種優化便會失去意義。這句話有個專業術語叫做as-if-serial semantics (as-if-serial語義)
3、重排序對多線程的影響
現在讓我們來看看,重排序是否會改變多線程程序的執行結果。請看下面的示例代碼:
- class ReorderExample {
- int a = 0;
- boolean flag = false;
- public void writer() {
- a = 1; // 1
- flag = true; // 2
- }
- public void reader() {
- if (flag) { // 3
- int i = a * a; // 4
- }
- }
- }
flag變量是個標記,用來標識變量a是否已被寫入。這裏假設有兩個線程A和B,A首先執行writer()方法,隨後B線程接着執行reader()方法。線程B在執行操作4時,能否看到線程A在操作1對共享變量a的寫入?
答案是:不一定能看到。
由於操作1和操作2沒有數據依賴關係,編譯器和處理器可以對這兩個操作重排序;同樣,操作3和操作4沒有數據依賴關係,編譯器和處理器也可以對這兩個操作重排序。讓我們先來看看,當操作1和操作2重排序時,可能會產生什麼效果?請看下面的程序執行時序圖:
上圖的執行順序是:2 -> 3 -> 4 -> 1 (這是完全存在並且合理的一種順序,如果你不能理解,請先了解CPU是如何對多個線程進行時間分配的)
如上圖所示,操作1和操作2做了重排序。程序執行時,線程A首先寫標記變量flag,隨後線程B讀這個變量。由於條件判斷爲真,線程B將讀取變量a。此時,變量a還根本沒有被線程A寫入,在這裏多線程程序的語義被重排序破壞了!
下面再讓我們看看,當操作3和操作4重排序時會產生什麼效果。下面是操作3和操作4重排序後,程序的執行時序圖:
在程序中,操作3和操作4存在控制依賴關係。當代碼中存在控制依賴性時,會影響指令序列執行的並行度。爲此,編譯器和處理器會採用猜測(Speculation)執行來克服控制相關性對並行度的影響。以處理器的猜測執行爲例,執行線程B的處理器可以提前讀取並計算a*a,然後把計算結果臨時保存到一個名爲重排序緩衝(reorder buffer ROB)的硬件緩存中。當接下來操作3的條件判斷爲真時,就把該計算結果寫入變量i中。
從圖中我們可以看出,猜測執行實質上對操作3和4做了重排序。重排序在這裏破壞了多線程程序的語義!
在單線程程序中,對存在控制依賴的操作重排序,不會改變執行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執行結果。
參考:
http://www.infoq.com/cn/articles/java-memory-model-2
http://blog.hesey.net/2011/07/reordering.html