**即時編譯器優化技術一覽:** ![](https://img2023.cnblogs.com/blog/3256961/202308/3256961-20230816153001309-163136082.png) ![](https://img2023.cnblogs.com/blog/325 ...
即時編譯器優化技術一覽:
相信許多同學看完這個表格,腦子裡面嗡嗡的,這些名字也是晦澀難懂,要實現這些優化的技術確實有比較大的難度,但是咱們只是學習,去理解這些技術,其實並不難,下麵咱們直接開講。
首先需要明確一點的,作者是為了講解方便,使用java的語法來表示優化技術所發揮出來的作用,實際上編譯優化並不是建立在java代碼之上的,而是建立在代碼的中間表示或者是機器碼之上的。
優化前:
優化後:
相信很容易看到優化後的不一樣,將get()直接優化成了.value,這個叫做方法內聯。
它的主要目的有兩個:
一是去除方法調用的成本(如查找方法版本、建立棧幀等);
二是為其他優化建立良好的基礎。方法內聯膨脹之後可以便於在更大範圍上進行後續的優化手段,可以獲取更好的優化效果。
因此各種編譯器一般都會把內聯優化放在優化序列最靠前的位置。
優化前:
優化後:
這個叫冗餘訪問消除,假設代碼中間註釋掉的“…do stuff…”所代表的操作不會改變b.value的值,那麼就可以把“z=b.value”替換為“z=y”,因為上一句“y=b.value”已經保證了變數y與b.value是一致的,這樣就可以不再去訪問對象b的局部變數了。
優化前:
優化後:
這個叫覆寫傳播,因為這段程式的邏輯之中沒有必要使用一個額外的變數z,它與變數y是完全相等的,因此我們可以使用y來代替z。
優化前:
優化後:
這個叫無用代碼消除,無用代碼可能是永遠不會被執行的代碼,也可能是完全沒有意義的代碼。
經過四次優化之後,前後的代碼所達到的效果是一致的,但是後者比前者省略了許多語句,體現在位元組碼和機器碼指令上的差距會更大,執行效率的差距也會更高。
接下來我們重點講解一下四項有代表性的優化技術:
一、方法內聯
內聯被業內戲稱為優化之母,因為除了消除方法調用的成本之外,它更重要的意義是為其他優化手段建立良好的基礎,我們可以回頭看看前面的案例,如果沒有最開始的方法內聯,後續多數其他優化都無法有效進行。
方法內聯的優化行為理解起來是沒有任何困難的,不過就是把目標方法的代碼原封不動地“複製”到發起調用的方法之中,避免發生真實的方法調用而已。但實際上Java虛擬機中的內聯過程卻遠沒有想象中容易,甚至如果不是即時編譯器做了一些特殊的努力,按照經典編譯原理的優化理論,大多數的Java方法都無法進行內聯。
對於一個虛方法,編譯器靜態地去做內聯的時候很難確定應該使用哪個方法版本,以之前例子所示的b.get()直接內聯為b.value為例,如果不依賴上下文,是無法確定b的實際類型是什麼的。
假如有ParentB和SubB是兩個具有繼承關係的父子類型,並且子類重寫了父類的get()方法,那麼b.get()是執行父類的get()方法還是子類的get()方法,這應該是根據實際類型動態分派的,而實際類型必須在實際運行到這一行代碼時才能確定,編譯器很難在編譯時得出絕對準確的結論。
於是,Java虛擬機引入了類型繼承關係分析技術,這是整個應用程式範圍內的類型分析技術(Class HierarchyAnalysis,CHA),用於確定在目前已載入的類中,某個介面是否有多於一種的實現、某個類是否存在子類、某個子類是否覆蓋了父類的某個虛方法等信息。如果是非虛方法,直接進行內聯就可以了;
如果遇到虛方法,則會向CHA查詢此方法在當前程式狀態下是否真的有多個目標版本可供選擇,如果只有一個版本,直接進行內聯。
不過由於Java程式是動態連接的,說不准什麼時候就會載入到新的類型從而改變CHA結論,因此這種內聯屬於激進預測性優化,必須預留好“逃生門”,即當假設條件不成立時的“退路”。假如在程式的後續執行過程中,虛擬機一直沒有載入到會令這個方法的接收者的繼承關係發生變化的類,那這個內聯優化的代碼就可以一直使用下去。如果載入了導致繼承關係發生變化的新類,那麼就必須拋棄已經編譯的代碼,退回到解釋狀態進行執行,或者重新進行編譯。
若CHA查詢出該方法確實有多個版本的目標方法,那即時編譯器還將進行最後一次努力,使用內聯緩存的方式來縮減方法調用的開銷。這種狀態下方法調用是真正發生了的,但是比起直接查虛方法表還是要快一些。
內聯緩存是一個建立在目標方法正常入口之前的緩存,它的工作原理大致為:在未發生方法調用之前,內聯緩存狀態為空,當第一次調用發生後,緩存記錄下方法接收者的版本信息,並且每次進行方法調用時都比較接收者的版本。如果以後進
來的每次調用的方法接收者版本都是一樣的,那麼這時它就是一種單態內聯緩存。通過該緩存來調用,比用不內聯的非虛方法調用,僅多了一次類型判斷的開銷而已。(這一點和sychronized鎖優化的偏向鎖思路相似)
但如果真的出現方法接收者不一致的情況,就說明程式用到了虛方法的多態特性,這時候會退化成超多態內聯緩存,其開銷相當於真正查找虛方法表來進行方法分派。
二、逃逸分析
逃逸分析的基本原理是:分析對象動態作用域,當一個對象在方法裡面被定義後,它可能被外部方法所引用,例如作為調用參數傳遞到其他方法中,這種稱為方法逃逸;
甚至還有可能被外部線程訪問到,譬如賦值給可以在其他線程中訪問的實例變數,這種稱為線程逃逸;
從不逃逸、方法逃逸到線程逃逸,稱為對象由低到高的不同逃逸程度。
如果能證明一個對象不會逃逸到方法或線程之外(換句話說是別的方法或線程無法通過任何途徑訪問到這個對象),或者逃逸程度比較低(只逃逸出方法而不會逃逸出線程),則可能為這個對象實例採取不同程度的優化,
如:
棧上分配:在Java虛擬機中,Java堆上分配創建對象的記憶體空間幾乎是Java程式員都知道的常識,Java堆中的對象對於各個線程都是共用和可見的,只要持有這個對象的引用,就可以訪問到堆中存儲的對象數據。
虛擬機的垃圾收集子系統會回收堆中不再使用的對象,但回收動作無論是標記篩選出可回收對象,還是回收和整理記憶體,都需要耗費大量資源。如果確定一個對象不會逃逸出線程之外,那讓這個對象在棧上分配記憶體將會是一個很不錯的主意,對象所占用的記憶體空間就可以隨棧幀出棧而銷毀。
在一般應用中,完全不會逃逸的局部對象和不會逃逸出線程的對象所占的比例是很大的,如果能使用棧上分配,那大量的對象就會隨著方法的結束而自動銷毀了,垃圾收集子系統的壓力將會下降很多。
棧上分配可以支持方法逃逸,但不能支持線程逃逸。
標量替換:若一個數據已經無法再分解成更小的數據來表示了,Java虛擬機中的原始數據類型(int、long等數值類型及reference類型等)都不能再進一步分解了,那麼這些數據就可以被稱為標量。
相對的,如果一個數據可以繼續分解,那它就被稱為聚合量,Java中的對象就是典型的聚合量。如果把一個Java對象拆散,根據程式訪問的情況,將其用到的成員變數恢復為原始類型來訪問,這個過程就稱為標量替換。
假如逃逸分析能夠證明一個對象不會被方法外部訪問,並且這個對象可以被拆散,那麼程式真正執行的時候將可能不去創建這個對象,而改為直接創建它的若幹個被這個方法使用的成員變數來代替。
同步消除:線程同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變數不會逃逸出線程,無法被其他線程訪問,那麼這個變數的讀寫肯定就不會有競爭,對這個變數實施的同步措施也就可以安全地消除掉。
三、公共子表達式消除
如果一個表達式E之前已經被計算過了,並且從先前的計算到現在E中所有變數的值都沒有發生變化,那麼E的這次出現就稱為公共子表達式。對於這種表達式,沒有必要花時間再對它重新進行計算,只需要直接用前面計算過的表達式結果代替E。
如果這種優化僅限於程式基本塊內,便可稱為局部公共子表達式消除,如果這種優化的範圍涵蓋了多個基本塊,那就稱為全局公共子表達式消除。
四、數組邊界檢查消除
Java語言是一門動態安全的語言,對數組的讀寫訪問也不像C、C++那樣實質上就是裸指針操作。
如果有一個數組foo[],在Java語言中訪問數組元素foo[i]的時候系統將會自動進行上下界的範圍檢查,即i必須滿足“i>=0 && i<foo.length”的訪問條件,否則將拋出一個運行時異常:java.lang.ArrayIndexOutOfBoundsException。
這對軟體開發者來說是一件很友好的事情,即使程式員沒有專門編寫防禦代碼,也能夠避免大多數的溢出攻擊。但是對於虛擬機的執行子系統來說,每次數組元素的讀寫都帶有一次隱含的條件判定操作,對於擁有大量數組訪問的程式代碼,這必定是一種性能負擔。
無論如何,為了安全,數組邊界檢查肯定是要做的,但數組邊界檢查是不是必須在運行期間一次不漏地進行則是可以“商量”的事情。例如下麵這個簡單的情況:數組下標是一個常量,如foo[3],只要在編譯期根據數據流分析來確定foo.length的值,並判斷下標“3”沒有越界,執行的時候就無須判斷了。
更加常見的情況是,數組訪問發生在迴圈之中,並且使用迴圈變數來進行數組的訪問。如果編譯器只要通過數據流分析就可以判定迴圈變數的取值範圍永遠在區間[0,foo.length)之內,那麼在迴圈中就可以把整個數組的上下界檢查消除掉,這可以節省很多次的條件判斷操作。
參考資料:
深入理解虛擬機-第3版-周志明著
為什麼寫文章 ?(若有錯誤,希望得到你的指正,若有問題,都可評論,我將會積極回覆)
在作者剛入行時,會遇到很多無法理解的問題,便經常向前輩請教問題,或是於網路之中苦苦尋找答案,經常被一些晦澀難懂的表達折磨的死去活來,現作者是一名擁有多年經驗的IT從業者,希望能夠將自己的知識以一種形象的方式輸出,先從虛擬機開始分享,之後會寫更多的專欄,最新的分享將會先在公眾號發佈,謝謝讀者的關註