J.U.C JMM. pipeline.指令重排序,happen-before

pipeline:html

      如今的CPU通常採用流水線方式來執行指令。一個指令執行週期被分紅:取值,譯碼,執行,訪存,寫會,更新PC若干階段。而後,多條指令能夠同時存在於流水線中,同時被執行,來提升系統的吞吐量。緩存

      流水線並非串行的,並不會由於一個耗時很長的執行在"執行"階段呆很長時間,而致使後續的指令被卡在"執行"階段以前上。相反,流水線是並行的,多條指令能夠同時處於同一階段,只要CPU內部的處理部件未被佔滿既可。好比說CPU有一個加法器和一個除法器,那麼一條加法指令和一條除法指令就可能同時處於"執行"階段,而兩條加法指令在"執行"階段就只能被串行工做。優化

      然而,這樣一來,亂序就可能產生了。好比一條加法指令原本出如今一條除法指令的後面,可是因爲除法的執行時間很長,在他執行完以前,加法可能先執行完了,再好比兩條訪存指令,可能因爲第二胎哦指令命中cache而致使他先於第一條指令完成。ui

     通常狀況下,指令亂序並非CPU在執行指令以前刻意去調整順序。CPU老是順序的去內存裏面取指令,而後將其順序的方法指令流水線。可是指令執行時的各類條件,指令與指令之間的相互影響,可能致使順序放入流水線的指令,最終亂序執行完成,這就是所謂的"順序流入,亂序流出"。spa

    指令流水線除了在資源不足的狀況下會卡主以外(如前所述的一個加法器應付兩條加法指令的狀況),指令之間的相關性也是致使流水線阻塞的重要緣由。.net

    CPU的亂序執行並非任意的亂序,而是以保障程序上下文因果關係爲前提的。有了這個前提,CPU執行的正確性纔有有保證:線程

a++; b = f(a); c--;

     因爲b = f(a)這條指令依賴於前一條指令a++的執行結果,因此b = f(a)將在"執行"階段以前被阻塞,知道a++的執行結果被生成出來;而c--跟前面沒有依賴,他能夠在b = f(a)以前就能執行完。像這樣有依賴關係的指令若是挨着很近,後一條指令一定會由於等待前一條執行的結果,而在流水線中阻塞好久,佔用流水線的資源。unix

     而編譯器的亂序,做爲編譯優化的一種手段,則試圖經過指令重排序將這樣的兩條指令拉開距離,以致於後一條指令進入CPU的時候,前一條指令結果已經能夠獲得了,那麼也就不須要阻塞等待了,好比指令重拍爲:code

a++ ; c-- ; b = f(a);

     相對於CPU的亂序,編譯器的亂序纔是真正的對指令順序作了調整。可是編譯器的亂序也必須保證程序上下文的因果關係不發生改變。htm

 

亂序的後果:

     亂序執行,有了"保證上下文英國關係"這一前提,通常狀況下不會有什麼問題的,所以,在絕大多數狀況下,咱們寫程序都不去考慮亂序所帶來的影響。可是,有些程序邏輯,單純從上下文是看不出他們的因果關係的。好比:

*addr = 5 ; val = *data;

     從表面上看,addr和data是沒有什麼聯繫的,徹底能夠放心的去亂序執行,可是若是這是在xx設備驅動程序中,這兩個變量可可能對應到設備的地址端口和數據端口。而且,這個設備規定了,當你須要讀寫設備上某個寄存器時,先將寄存器編號設置到地址端口,而後就能夠經過對數據端口的讀寫而操做對應的寄存器,那麼這麼一來,對前面那兩條指令的亂序執行就可能形成錯誤。對於這樣的邏輯,咱們姑且將其稱做隱式的因果關係;而指令與指令之間直接的輸入輸出依賴,稱之爲顯式的因果關係。CPU或者編譯器的亂序是以保證顯式的因果關係不變爲前提的,可是他們都沒法識別隱式的因果關係。再舉個例子:

object -> data = xxx;  object -> ready = true;

    當設置了data以後,記下標誌,而後在另外一個線程中可能執行:

if (object -> ready) do_something(object -> data);

    若是考慮到亂序,若是標誌被賦值先於data被賦值, 那麼結果就可能杯具了,由於從字面上看,前面的那兩條指令其實並不存在顯式的因果關係,亂序是有可能發生的。

    總的來講,若是程序有顯式的因果關係的話,亂序必定會尊重這些關係;不然,亂序就可能打破程序原有的邏輯。這時候,就須要使用屏障來抑制亂序,以維持程序所指望的邏輯。

 

Memory barrier:

    內存屏障主要有:讀屏障,寫屏障,通用屏障,優化屏障;

    以讀屏障爲例,他用於保證讀操做有序。屏障以前的讀操做必定會先於屏障以後的讀操做完成,寫操做不受影響,同屬於屏障的某一側的讀操做中也不受影響。相似的,寫屏障用於限制寫操做。而通用屏障則對讀寫操做都有做用。而優化屏障則用於限制編譯器的指令重排,不區分讀寫。前三種屏障都隱含了優化屏障的功能,好比:

tmp = ttt ;  *addr = 5 ; memoryBarrier(); var = *data;

    有了內存屏障就能夠確保先設置地址端口,再讀取數據端口。而至於設置地址讀卡和tmp的賦值孰先孰後,屏障則不作干預。

    有了內存屏障,就能夠在隱式的因果關係的場景中,保證因果關係邏輯正確

 

多處理器狀況:

      前面只是考慮了單處理器指令亂序的問題,而在多處理器下,除了每一個處理器要獨自面對上面討論的問題以外,當處理器以前存在交互的時候,一樣要面對亂序的問題。

      一個處理器(記爲a)對內存的寫操做證並非直接就在內存上生效的,而是要先通過自身的cache。另外一個處理器(記爲b)若是要獨缺相應內存上的新值,先得等a的cache同步到內存,而後b的cache再從內存同步這個新值。而若是須要同步的值不止一個的話,就會存在順序問題。再舉前面的一個例子:

 <cpu - a>   *************************************** <cpu - b>
 object -> data = xxx;            
 write-memory-barrier();                          if (object -> ready)
 object -> ready = true;                          do_something(object -> data);

      前面也說過,必需要使用屏障來保證CPU-a不發生亂序,從而使得ready標記賦值時候,data必定是有效的。可是在多處理器狀況下,這還不夠。data和ready標記的新值可能以相反的順序更新到CPU-b上!

      其實這種狀況在大多數體系結構下並不會發生,不過內核文檔memory-barriers舉了一個alpha機器的例子。alpha機器可能使用分列的cache結構,每一個cache列能夠並行工做,以提升效率。而每一個cache列上面的緩存的數據是互斥的(若是不互斥就還得解決cache列之間的一致性),因而就可能引起cache更新不一樣步的問題。

      假設cahce被分爲兩列,而CPU-a和CPU-b上的data和ready都分別被緩存到不一樣的cache列中。首先是CPU-a更新了cache以後,會發送消息讓其餘CPU的cache來同步新的值。可是如今假設了有兩個cache列,可能因爲緩存data的cache列比較繁忙而使得data的更新消息晚於ready發出,那麼程序邏輯就無法保證了。不過在SMP下的內存屏障在解決指令亂序的問題在外,也將cache更新消息亂序的問題解決了。只要使用了屏障,就能保證屏障以前的cache更新消息先於屏障支護的消息被髮出。

      而後就是CPU-b的問題。在使用了屏障以後,CPU-a已經保證data的更新消息先發出了,那麼CPU-b也會先收到data的更新消息。不過一樣,CPU-b上緩存data的cahce列可能比較繁忙,致使對data的更新晚於對ready的更新,這裏一樣會出問題。

      因此,在這種狀況下,CPU-b也得使用屏障,CPU-a使用寫屏障,保證兩個寫操做不亂序,而且相應的兩個cache列的更新消息不亂序;CPU-b上則須要使用讀屏障,保證對兩個cache單元的同步不亂序,可見,SMP下的內存屏障必定是須要配對使用的。

 <cpu - a> ************************************************* <cpu - b>
 object -> data = xxx;                                      if (object -> ready)
 write-memory-barrier();                                    read-memory-barrier();
 object -> ready = true;                                     do_something(object -> data);

 

 原文:http://blog.csdn.net/jiang_bing/article/details/8629425

相關文章
相關標籤/搜索