本篇將介紹程式編譯時期的程式碼優化手段,分成兩個階段:
- 概述
- 早期(編譯期)優化
- 晚期(執行期)優化
1.概述
a.由於對Java語言的編譯期理解不同,可以分出幾個時期:
- 前端編譯器
- 作用:把Java程式碼轉變成位元組碼
- 代表:Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ)
- 該時期的優化主要用於提升程式的編碼效率
- 後端執行期編譯器/JIT編譯器
- 作用:把位元組碼轉變成本地機器碼
- 代表:HotSpot VM的C1、C2編譯器
- 該時期的優化主要用於提升程式的執行效率
- 靜態提前編譯器/AOT編譯器
- 作用:直接把Java程式碼編譯成本地機器碼
- 代表:GNU Compiler for the Java(GCJ)、Excelsior JET
b.Java即時編譯器與C/C++靜態編譯器的對比
- 即時編譯器執行需要佔用程式執行時間,使得優化手段受制於編譯成本,否則使用者將在啟動程式察覺到重大延遲;而靜態編譯器的編譯時間成本不是重點
- 靜態編譯器所有優化都在編譯期完成,而即時編譯器的動態性是把雙刃劍,一方面要求虛擬機器頻繁進行動態檢查從而消耗大量執行時間,而且難以全域性優化、只能以激進優化來完成,另一方面擁有執行期效能監控的優化措施,如呼叫頻率預測、分支頻率預測、裁剪未被選擇的分支等
- Java中使用虛方法的頻率遠大於C/C++,表示執行時對方法接收者進行多型選擇的頻率更大,因此在進行某些優化難度會更大
- Java在堆上進行物件的記憶體分配,而C/C++可在堆、棧上分配,減輕了記憶體回收的壓力;且C/C++中主要由使用者程式程式碼回收記憶體,不存在無用物件的篩選,相比於垃圾收集機制執行效率更高
2.早期(編譯期)優化
幾乎所有語言都提供一些語法糖來方便開發,或能提高效率、或能提升語法的嚴謹性、或能減少編碼出錯的機會,下面是幾種常見語法糖:
- 泛型與型別擦除
- C#的泛型是真實泛型:無論在程式原始碼、編譯後的IL、還是執行期的CLR中都是切實存在的,List<int>和List<String>在系統執行期生成,有自己的虛方法表和型別資料,屬於不同的型別,這種實現稱為型別膨脹
- Java的泛型是偽泛型:只在程式原始碼中存在,在編譯後的位元組碼檔案中就已替換為原生型別,並在相應的地方插入了強制轉型程式碼,因此ArrayList<int>與ArrayList<String>是同一個類,這種實現稱為型別擦除
- 自動裝箱、拆箱
- 遍歷迴圈
- 條件編譯:使用條件為常量的if語句
3.晚期(執行期)優化
a.HotSpot虛擬機器採用直譯器與編譯器並存的架構,互動情況:
- 當程式需要迅速啟動和執行時,直譯器可以先發揮作用,從而省去編譯時間
- 程式執行後,隨著時間的推移,編譯器逐漸發揮作用,把更多程式碼編譯成原生程式碼,從而獲取更高的執行效率
- 如果程式執行環境受記憶體資源限制較大,可以用解釋執行節約記憶體,反之可以用編譯執行提升效率
- 直譯器可作為編譯器激進優化的逃生門,當激進優化不成立時,如載入新類後型別繼承結構出現變化、出現罕見陷阱,可通過逆優化退回到解釋狀態繼續執行。如圖:
有上圖可見,HotSpot虛擬機器中內建了兩個即時編譯器:Client Compiler(C1編譯器和)和Server Compiler(C2編譯器),搭配模式:
- 混合模式(Mixed Mode):預設採用直譯器與其中一個編譯器進行配合工作,虛擬機器會根據自身版本與宿主機器的硬體效能自動選擇執行模式和編譯器,使用者可以使用
-client
或-server
引數去強制指定虛擬機器執行在Client模式或Server模式。- 解釋模式(Interpreted Mode):使用引數
-Xint
,編譯器不工作,都使用解釋方式執行。- 編譯模式(Compiled Mode):使用引數
-Xcomp
,優先採用編譯方式執行,但直譯器仍然要在編譯無法進行的情況下介入執行過程。
b.HotSpot即時編譯器的編譯物件:熱點程式碼
- 分類:
- 被多次呼叫的方法:採用JIT編譯方式,以整個方法作為編譯物件
- 被多次執行的迴圈體:採用OSR編譯方式,發生在方法執行過程中,仍以整個方法作為編譯物件
- 判斷方式:通過熱點探測
- 基於取樣的熱點探測(Sample Based Hot Spot Detection):週期性檢查各個執行緒的棧頂,常出現在棧頂的方法就是熱點方法
- 好處:實現簡單、高效、易於獲取方法呼叫關係
- 缺點:難以精確確認某個方法的熱度、易受到執行緒阻塞或外界影響而擾亂熱點探測
- 基於計數器的熱點探測(Counter Based Hot Spot Detection):為每個方法建立計數器來統計方法的執行次數,執行次數超過一定的閾值就是熱點方法
- 優點:精確、嚴謹
- 缺點:實現較麻煩、不能直接獲取到方法的呼叫關係
- 計數器型別:
- 方法呼叫計數器(Invocation Counter):統計方法被呼叫的次數,當計數器超過閾值會觸發JIT編譯
- 回邊計數器(Back Edge Counter):統計方法中迴圈體程式碼執行的次數,當計數器超過閾值會觸發OSR編譯
- 基於取樣的熱點探測(Sample Based Hot Spot Detection):週期性檢查各個執行緒的棧頂,常出現在棧頂的方法就是熱點方法
c.HotSpot即時編譯器的編譯過程
- Client Compiler:主要進行區域性優化、放棄耗時較長的全域性優化。採用簡單快速的三段式編譯:
- 第一個階段:一個平臺獨立的前端把位元組碼構造成一種高階中間程式碼表示(HIR),在此之前會在位元組碼上完成一部分基礎優化,如方法內聯、常量傳播等
- 第二個階段:一個平臺相關的後端從HIR中產生低階中間程式碼表示(LIR),在此之前會在HIR上完成另外一些優化,如空值檢查消除、範圍檢查消除等,以便讓HIR達到更高效的程式碼表示形式
- 第三個階段:平臺相關的後端使用線性掃描演算法在LIR 上分配暫存器,並在LIR上做窺孔優化,然後產生機器程式碼。大致執行過程如圖:
- Server Compiler:專門面向服務端的典型應用並且特別為服務端的效能配置調整過,是一個充分優化過的高階編譯器,體現在:
- 會執行所有經典的優化動作:如無用程式碼消除、迴圈展開、迴圈表示式外提、消除公共子表示式、常量傳播、基本塊重排序等
- 會實施與Java特性密切相關的優化技術:如範圍檢查消除、空值檢查消除等
- 根據直譯器或Client Compiler提供的效能監控資訊可能會進行一些不穩定的激進優化:如守護內聯、分支頻率預測等
另外,Server Compiler的暫存器分配器是一個全域性圖著色分配器,能夠充分利用某些處理器架構上的大暫存器集合。雖然Server Compiler的編譯時間比較緩慢,但是其編譯速度遠超於傳統的靜態優化編譯器,且比Client Compiler編譯輸出的程式碼質量更高,能減少原生程式碼的執行時間,從而抵消了額外的編譯時間開銷。
d.HotSpot虛擬機器即時編譯器在生成程式碼時採用的程式碼優化技術:
其中幾種最有代表性的優化技術:
- 語言無關的經典優化技術之一:公共子表示式消除(Common Subexpression Elimination)
- 含義:若一個表示式E已經計算過且E中所有變數值未發生任何變化,則稱E為公共子表示式,此時沒必要花時間再次計算,直接用之前計算過的表示式結果代替E即可
- 型別:
- 區域性公共子表示式消除:優化僅限於程式的基本塊內
- 全域性公共子表示式消除:優化的範圍涵蓋了多個基本塊
- 語言相關的經典優化技術之一 :陣列邊界檢查消除(Array Bounds Checking Elimination)
- 若陣列下標是個常量,只要在編譯期根據資料流分析確定這個陣列的長度,且判斷得出該陣列下標未越界,那麼執行時無需再檢查
- 若陣列訪問發生在迴圈中且使用迴圈變數來進行陣列訪問,只要在編譯期根據資料流分析確定迴圈變數的取值範圍永遠在區間[0,陣列長度)內,那麼在整個迴圈中無需再進行多次檢查
- 最重要的優化技術之一:方法內聯(Method Inlining)
- 含義:把目標方法的程式碼復
- 制到發起呼叫的方法之中,避免發生真實的方法呼叫
- 主要目的:去除方法呼叫的成本,如建立棧幀等;為其他優化建立良好的基礎,便於在更大範圍上採取後續的優化手段、獲取更好的優化效果
- 最前沿的優化技術之一:逃逸分析(Escape Analysis)
- 基本行為:分析物件動態作用域
- 型別:
- 方法逃逸:一個物件在方法中被定義後,可能被外部方法所引用。如作為呼叫引數傳遞到其他方法中
- 執行緒逃逸:一個物件在方法中被定義後,能被外部執行緒訪問到。如賦值給類變數或可以在其他執行緒中訪問的例項變數
- 對能夠證明不會逃逸到方法或執行緒之外的物件可進行的優化手段:
- 棧上分配(Stack Allocation):在棧上對該物件進行記憶體分配,此時該物件所佔用的記憶體空間會隨棧幀出棧而銷燬,可減少垃圾收集系統的壓力
- 同步消除(Synchronization Elimination):在該物件上不會有讀寫競爭,可消除掉對該物件的同步措施,從而減少資源的消耗
- 標量替換(Scalar Replacement):若該物件可以進一步分解,那麼直接建立它的若干個被這個方法使用到的成員變數來替換