天天看點

什麼是指令重排?

目錄

案例

什麼是指令重排?

擴充

什麼是JIT?

為什麼HotSpot虛拟機要使用解釋器與編譯器并存的架構?

編譯的時間開銷

什麼是并行指令集?

那麼什麼是并行指令集的重排序呢?

as-if-serial語義

結果

定義四個int類型的變量,初始化都為0。

定義兩個線程t1、t2

t1線程修改a和x的值

t2線程修改b和y的值

分别啟動兩個線程。

正常情況下,x和y的值,會根據t1和t2線程的執行情況來決定。

如果t1線程優先執行,那麼得到的結果是x=0、y=1。

如果t2線程優先執行,那麼得到的結果是x=1、y=0。

如果t1和t2線程同時執行,那麼得到的結果是x=1、y=1。

為什麼?結果為什麼是 0 和 0。

其實這就是所謂的指令重排序問題,假設上面的代碼通過指令重排序之後,變成下面這種結構:

經過重排序之後,如果t1和t2線程同時運作,就會得到x=0、y=0的結果,這個結果從人的視角來看,就有點類似于t1線程中a=1的修改結果對t2線程不可見,同樣t2線程中b=1的執行結果對t1線程不可見。

指令重排序是指編譯器或CPU為了優化程式的執行性能而對指令進行重新排序的一種手段,重排序會帶來可見性問題,是以在多線程開發中必須要關注并規避重排序。

從源代碼到最終運作的指令,會經過如下兩個階段的重排序。

第一階段,編譯器重排序,就是在編譯過程中,編譯器根據上下文分析對指令進行重排序,目的是減少CPU和記憶體的互動,重排序之後盡可能保證CPU從寄存器或緩存行中讀取資料。

在前面分析JIT優化中提到的循環表達式外提(Loop Expression Hoisting)就是編譯器層面的重排序,從CPU層面來說,避免了處理器每次都去記憶體中加載stop,減少了處理器和記憶體的互動開銷。

第二階段,處理器重排序,處理器重排序分為兩個部分。

并行指令集重排序,這是處理器優化的一種,處理器可以改變指令的執行順序。

記憶體系統重排序,這是處理器引入Store Buffer緩沖區延時寫入産生的指令執行順序不一緻的問題。

1、動态編譯(dynamic compilation)指的是“在運作時進行編譯”;與之相對的是事前編譯(ahead-of-time compilation,簡稱AOT),也叫靜态編譯(static compilation)。

2、JIT 編譯(just-in-time compilation)狹義來說是當某段代碼即将第一次被執行時進行編譯,因而叫“即時編譯”。JIT編譯是動态編譯的一種特例。JIT編譯一詞後來被泛化,時常與動态編譯等價;但要注意廣義與狹義的JIT編譯所指的差別。

3、自适應動态編譯(adaptive dynamic compilation)也是一種動态編譯,但它通常執行的時機比JIT編譯遲,先讓程式“以某種式”先運作起來,收集一些資訊之後再做動态編譯。這樣的編譯可以更加優化。

什麼是指令重排?

在部分商用虛拟機中(如HotSpot),Java程式最初是通過解釋器(Interpreter)進行解釋執行的,當虛拟機發現某個方法或代碼塊的運作特别頻繁時,就會把這些代碼認定為“熱點代碼”。為了提高熱點代碼的執行效率,在運作時,虛拟機将會把這些代碼編譯成與本地平台相關的機器碼,并進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器(Just In Time Compiler,下文統稱JIT編譯器)。

即時編譯器并不是虛拟機必須的部分,Java虛拟機規範并沒有規定Java虛拟機内必須要有即時編譯器存在,更沒有限定或指導即時編譯器應該如何去實作。但是,即時編譯器編譯性能的好壞、代碼優化程度的高低卻是衡量一款商用虛拟機優秀與否的最關鍵的名額之一,它也是虛拟機中最核心且最能展現虛拟機技術水準的部分。

由于Java虛拟機規範并沒有具體的限制規則去限制即使編譯器應該如何實作,是以這部分功能完全是與虛拟機具體實作相關的内容,如無特殊說明,我們提到的編譯器、即時編譯器都是指Hotspot虛拟機内的即時編譯器,虛拟機也是特指HotSpot虛拟機。

盡管并不是所有的Java虛拟機都采用解釋器與編譯器并存的架構,但許多主流的商用虛拟機(如HotSpot),都同時包含解釋器和編譯器。

解釋器與編譯器兩者各有優勢:當程式需要 迅速啟動和執行 的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行。在程式運作後,随着時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之後,可以擷取 更高的執行效率 。當程式運作環境中 記憶體資源限制較大 (如部分嵌入式系統中),可以使用 解釋器執行節約記憶體 ,反之可以使用 編譯執行來提升效率 。此外,如果編譯後出現“罕見陷阱”,可以通過逆優化退回到解釋執行。

HotSpot虛拟機中内置了兩個即時編譯器:Client Complier和Server Complier,簡稱為C1、C2編譯器,分别用在用戶端和服務端。目前主流的HotSpot虛拟機中預設是采用解釋器與其中一個編譯器直接配合的方式工作。程式使用哪個編譯器,取決于虛拟機運作的模式。HotSpot虛拟機會根據自身版本與主控端器的硬體性能自動選擇運作模式,使用者也可以使用“-client”或“-server”參數去強制指定虛拟機運作在Client模式或Server模式。

用Client Complier擷取更高的編譯速度,用Server Complier 來擷取更好的編譯品質。為什麼提供多個即時編譯器與為什麼提供多個垃圾收集器類似,都是為了适應不同的應用場景。

解釋器的執行,抽象的看是這樣的:

*輸入的代碼 -> [ 解釋器 解釋執行 ] -> 執行結果

而要JIT編譯然後再執行的話,抽象的看則是:

*輸入的代碼 -> [ 編譯器 編譯 ] -> 編譯後的代碼 -> [ 執行 ] -> 執行結果

*說JIT比解釋快,其實說的是“執行編譯後的代碼”比“解釋器解釋執行”要快,并不是說“編譯”這個動作比“解釋”這個動作快。

JIT編譯再怎麼快,至少也比解釋執行一次略慢一些,而要得到最後的執行結果還得再經過一個“執行編譯後的代碼”的過程。是以,對“隻執行一次”的代碼而言,解釋執行其實總是比JIT編譯執行要快。

怎麼算是“隻執行一次的代碼”呢?粗略說,下面兩個條件同時滿足時就是嚴格的“隻執行一次”

1、隻被調用一次,例如類的構造器(class initializer,())

2、沒有循環

對隻執行一次的代碼做JIT編譯再執行,可以說是得不償失。

對隻執行少量次數的代碼,JIT編譯帶來的執行速度的提升也未必能抵消掉最初編譯帶來的開銷。

隻有對頻繁執行的代碼,JIT編譯才能保證有正面的收益。

在處理器核心中一般會有多個執行單元,比如算術邏輯單元、位移單元等。

在引入并行指令集之前,CPU在每個時鐘周期内隻能執行單條指令,也就是說隻有一個執行單元在工作,其他執行單元處于空閑狀态;

在引入并行指令集之後,CPU在一個時鐘周期内可以同時配置設定多條指令在不同的執行單元中執行。

如下圖所示,假設某一段程式有多條指令,不同指令的執行實作也不同。

什麼是指令重排?

對于一條從記憶體中讀取資料的指令,CPU的某個執行單元在執行這條指令并等到傳回結果之前,按照CPU的執行速度來說它足夠處理幾百條其他指令,而CPU為了提高執行效率,會根據單元電路的空閑狀态和指令能否提前執行的情況進行分析,把那些指令位址順序靠後的指令提前到讀取記憶體指令之前完成。

實際上,這種優化的本質是通過提前執行其他可執行指令來填補CPU的時間空隙,然後在結束時重新排序運算結果,進而實作指令順序執行的運作結果。

as-if-serial表示所有的程式指令都可以因為優化而被重排序,但是在優化的過程中必須要保證是在單線程環境下,重排序之後的運作結果和程式代碼本身預期的執行結果一緻,Java編譯器、CPU指令重排序都需要保證在單線程環境下的as-if-serial語義是正确的。

可能有些讀者會有疑惑,既然能夠保證在單線程環境下的順序性,那為什麼還會存在指令重排序呢?在JSR-133規範中,原文是這麼說的。

The compiler, runtime, and hardware are supposed to conspire to create the illusion of as-if-serial semantics, which means that in a single-threaded program, the program should not be able to observe the effects of reorderings.However, reorderings can come into play in incorrectly synchronized multithreaded programs, where one thread is able to observe the effects of other threads, and may be able to detect that variable accesses become visible to other threads in a different order than executed or specified in the program.

編譯器、運作時和硬體應該合力創造as-if-serial語義的錯覺,這意味着在單線程程式中,程式不應該能夠觀察到重新排序的效果。然而,重新排序可以 在不正确同步的多線程程式中發揮作用,其中一個線程能夠觀察其他線程的影響,并且可能能夠檢測到變量通路對其他線程以與程式中執行或指定的順序不同的順序變得可見。

as-if-serial語義允許重排序,CPU層面的指令優化依然存在。在單線程中,這些優化并不會影響整體的執行結果,在多線程中,重排序會帶來可見性問題。

另外,為了保證as-if-serial語義是正确的,編譯器和處理器不會對存在依賴關系的操作進行指令重排序,因為這樣會影響程式的執行結果。我們來看下面這段代碼:

上述代碼按照正常的執行順序應該是1、2、3,在多線程環境下,可能會出現2、1、3這樣的執行順序,但是一定不會出現3、2、1這樣的順序,因為3與1和2存在資料依賴關系,一旦重排序,就無法保證as-if-serial語義是正确的。