[OS] 高級語言虛擬機的編譯優(yōu)化

1. JIT編譯器

JVM中的仿真引擎可以通過許多方式來實現(xiàn),具有不同的復(fù)雜性和性能級別。
最簡單的方法是使用對字節(jié)碼指令的直接解釋。
一個更高級的并且被普遍使用的仿真方法是執(zhí)行即時(JIT)編譯。

利用JIT編譯器,方法在第一被調(diào)用時編譯,即為了執(zhí)行而“即時”。
這種編譯實質(zhì)上執(zhí)行了從字節(jié)碼指令到本地主機指令的翻譯。

JIT編譯器與二進制翻譯器緊密相關(guān),并且可能容易被稱為JIT“翻譯器”。
實際上,一個稱為“編譯器”而另一個稱為“編譯器”的理由似乎是由兩個不同組的人起的名字。

JIT編譯器與傳統(tǒng)編譯器的不同在于,它沒有解析一個高級語言程序并在將它轉(zhuǎn)化為中間形式之前進行語法檢查的前端。
已經(jīng)通過加載器的字節(jié)碼程序被認為是正確的。
盡管大多數(shù)JIT編譯器在進行優(yōu)化之前會將字節(jié)碼指令轉(zhuǎn)化為一種不同的中間形式,字節(jié)碼指令本身實質(zhì)上仍是一種中間表示。

動態(tài)的運行時編譯器可以執(zhí)行大多數(shù)(如果不是全部)被經(jīng)典的靜態(tài)編譯器所執(zhí)行的優(yōu)化,
以及其他與Java程序有關(guān)的優(yōu)化。
然而許多優(yōu)化是消耗時間的,這增加了執(zhí)行一個Java程序的運行時開銷。

因此,JIT編譯器會包括多個優(yōu)化級別,最復(fù)雜的優(yōu)化被應(yīng)用于頻繁執(zhí)行的方法。
這導(dǎo)致了一種應(yīng)用在方法層的分階段優(yōu)化的形式。
一種更加高效的策略是將優(yōu)化選擇性的只應(yīng)用于頻繁使用的代碼區(qū)域,而不是可能包含這種區(qū)域的整個方法。

典型的高性能仿真引擎從解釋開始,并附加上用來定位頻繁使用的方法的剖析過程。
然后當一個給定方法達到使用門限時,這個方法用最小的優(yōu)化來編譯。
稍后,依賴于使用的級別,熱點方法內(nèi)被選定的代碼段被進一步優(yōu)化。

2. 剖析

剖析(profiling)是為執(zhí)行中的程序收集指令和數(shù)據(jù)的統(tǒng)計信息的過程。
這種統(tǒng)計剖析數(shù)據(jù)可以作為代碼優(yōu)化過程的輸入。

一般的說,由于程序的可預(yù)測性,會使基于剖析的優(yōu)化產(chǎn)生效果。
亦即,對程序的過去行為度量所得到的程序特性常會繼續(xù)保持在程序的未來行為中,
因此,可以用這些信息來指導(dǎo)優(yōu)化。

傳統(tǒng)上,在軟件開發(fā)者的控制下,代碼剖析已作為向編譯過程提供反饋信息的一種途徑。
編譯器首先將源程序分解成一個控制流圖,然后分析這個圖以及程序的其他方面,
接著在程序中插入探針(probe),來收集剖析信息。

探針是一段短的代碼序列,它將程序的執(zhí)行信息記錄到內(nèi)存中的一個剖析日志中。

例如,可以在分支指令處安插剖析探針來記錄分支的結(jié)果。
在編譯器生成的代碼中,將包含這些插入的探針。
接下來,程序在一個典型的數(shù)據(jù)輸入集上運行,與此同時探針為整個程序生成剖析數(shù)據(jù)。

然后,對剖析日志進行離線分析,分析結(jié)果將反饋回編譯器中。
編譯器再使用這些信息來生成優(yōu)化代碼。

優(yōu)化器可以對原始的高級語言程序進行優(yōu)化,更多的是從編譯器生成中間形式或最初編譯得到的二進制代碼開始進行優(yōu)化。
在某些情況下,硬件可以通過計數(shù)器或者時鐘中斷來支持剖析收集,其中允許通過軟件來收集統(tǒng)計采樣信息。

3. 優(yōu)化

正如大多數(shù)其他虛擬機應(yīng)用那樣,在高級語言虛擬機中性能是一個重要的考慮因素。
當處理高級語言虛擬機時有兩個挑戰(zhàn)。
第一個與其他動態(tài)優(yōu)化虛擬機相同,用改善程序執(zhí)行時間來彌補運行時優(yōu)化的開銷。
第二個挑戰(zhàn)是使面向?qū)ο蟪绦蚩焖俚膱?zhí)行。

面向?qū)ο蟪绦蛲ǔ0l繁使用對指令和數(shù)據(jù)的間接尋址,以及頻繁使用小方法(它有較高的方法調(diào)用開銷)。

3.1 代碼重排

當代碼重排被應(yīng)用于高級語言虛擬機的上下文時,它是一個簡單且非常有效的優(yōu)化。
大多數(shù)代碼重排算法“平整”代碼,使得沿著最常走的控制流路徑的基本塊是在內(nèi)存中連續(xù)的位置上。
其好處是更加高效的取指,這是由于提高了時間和空間的局部性,并且改進了條件分支的可預(yù)測性。

代碼重排在所有的優(yōu)化中經(jīng)常提供一個較高性能的收益。

3.2 方法內(nèi)聯(lián)

方法內(nèi)聯(lián)也被稱為過程內(nèi)聯(lián)。
通過內(nèi)聯(lián),一個方法調(diào)用被替換為這個方法中的實際代碼。
即,方法代碼被“內(nèi)聯(lián)”放到主調(diào)代碼中。

方法內(nèi)聯(lián)省去了傳遞參數(shù),管理棧幀以及實際的控制轉(zhuǎn)移等開銷,而這可能以較大的二進制程序映像為代價。

面向?qū)ο缶幊掏膭钍褂迷S多小的方法,因此通過避免所有這種代碼,
即與一個方法調(diào)用關(guān)聯(lián)的調(diào)用序列,經(jīng)??梢燥@著的改善性能。
內(nèi)聯(lián)的另一個重要的好處是它增大了稍后代碼分析和優(yōu)化可以發(fā)生的范圍。

內(nèi)聯(lián)那些本身代碼的大小比方法調(diào)用序列還要小的小方法,幾乎總是有收益的。
當被內(nèi)聯(lián)時,這種小方法不但會更快的執(zhí)行,而且還會比起初的(沒有內(nèi)聯(lián)的)代碼消耗更少的指令空間,
這使指令cache的行為得到改進(或者至少沒有退化)。

對于較大的方法,內(nèi)聯(lián)的好處被削弱了,因為調(diào)用序列在整個執(zhí)行時間中占的百分比較小,
而且整個代碼尺寸(和指令cache需求)會增長。
如果不選擇的應(yīng)用,內(nèi)聯(lián)較大的方法,尤其是那些從許多不同位置被調(diào)用的方法,就可能導(dǎo)致代碼爆炸(code explosion),
使得cache行為較差而且性能受損。

因此,為了在中等及大的方法上應(yīng)用內(nèi)聯(lián),需要某種成本效益分析,而成本效益的關(guān)系是相當復(fù)雜的。
在代碼爆炸方面的代價可以相對容易的被消除,但是怎么把這個轉(zhuǎn)化成性能代價是比較復(fù)雜的,
通常是由離線實驗來得到一個規(guī)劃函數(shù)。

效益主要是方法大小和方法被調(diào)用頻率的一個函數(shù),
亦即,最經(jīng)常被調(diào)用的方法將導(dǎo)致最大的效益,并且是內(nèi)聯(lián)的主要候選者。

3.3 優(yōu)化虛擬方法調(diào)用

通常,內(nèi)聯(lián)可以很容易的應(yīng)用于靜態(tài)方法和被程序員聲明為final方法的方法。
當從給定的調(diào)用點調(diào)用這些方法之一時,被調(diào)用的方法代碼絕不會改變。
因此,一旦在這些方法中的一個上執(zhí)行內(nèi)聯(lián)時,被內(nèi)聯(lián)的代碼將總是正確的代碼。

然而在面向?qū)ο笳Z言中,許多方法不是靜態(tài)的或者是final的,即與動態(tài)類相關(guān)聯(lián)的方法。
由于類層次和由此產(chǎn)生的多態(tài),被一個虛擬方法調(diào)用執(zhí)行的實際代碼可能改變,這依賴于被引用對象的具體子類。

決定使用哪個代碼是在運行時通過一個動態(tài)方法表查找來確定的。

因為一個虛擬方法的代碼可以被動態(tài)改變,這取決于所給的對象類型,
所以,對任何invokevirtual指令調(diào)用的方法,將看來是禁止方法內(nèi)聯(lián)的。

不過在許多情況下,虛擬方法在大多數(shù)時間(或者全部時間)里是用同類對象來調(diào)用的。
這種情況可以通過剖析調(diào)用一個給定的虛擬方法所引用的類型來確定。

如果一個特殊的方法在大多數(shù)時間內(nèi)被調(diào)用,那么可以內(nèi)聯(lián)這個方法在最普通的子類中的方法代碼,
并把守護指令放置在被內(nèi)聯(lián)的代碼上方。

守護簡單的測試緊接著將要調(diào)用的方法所基于的引用類型,
如果它是所期望的(普通的)類型,那么守護將讓控制轉(zhuǎn)到內(nèi)聯(lián)的方法版本。
另一方面,在這種引用是不同于所期望的子類時的少見情況下,
守護可以分支到一條非內(nèi)聯(lián)的invokevirtual指令。

如果方法調(diào)用真的是多態(tài)以至于完全內(nèi)聯(lián)是沒有用的,那么至少可以避免動態(tài)方法表查找的開銷,
這要使用類似于二進制翻譯中的軟件跳轉(zhuǎn)預(yù)測技術(shù),這種技術(shù)被稱為多態(tài)內(nèi)聯(lián)高速緩存(PIC)。
對于PIC,運行時系統(tǒng)負責(zé)維護存根(stub)代碼,將最經(jīng)常使用的跳轉(zhuǎn)放在序列的頂部。

3.4 多版本和專門化

(1)多版本
前述的虛擬方法調(diào)用的內(nèi)聯(lián)方法本質(zhì)上是一種多版本形式。

多版本有兩種(或多種)代碼版本,并且一個版本的選擇取決于運行時信息,如數(shù)據(jù)的值或類型信息。
對于內(nèi)聯(lián)的方法,一個版本是被內(nèi)聯(lián)的方法代碼,另一個版本是一條invokevirtual指令,而守護選擇其中一個版本。

可以把這個同樣的一般方法應(yīng)用于其他類型的代碼,而不只是虛擬方法調(diào)用。

例如,剖析數(shù)據(jù)值可以確定數(shù)組A的元素幾乎總是零。
守護檢查A[i]看其是否為零,如果是這樣,則它使用一個代碼版本跳過剩余的指令并且簡單的將B[i]設(shè)為零。

(2)專門化
多版本的一個重要方面是專門化。

如果某些變量或引用總是被分配為已知是常數(shù)(或者來自一個有限的范圍)的數(shù)據(jù)值或類型,
那么有時可能使用簡化的特殊情況來代替比較復(fù)雜的一般代碼。

上圖中,A[i]為零時是特殊情況。

專門化可以與多版本聯(lián)合使用,或者可以通過某種代碼分析來激活,
這種分析指示只需要單個專門的版本。

構(gòu)造多個版本的一種可選方法是只編譯單個代碼版本(或者少數(shù)版本),并推遲對一般的且更復(fù)雜的情況的編譯。
例如,剖析可以發(fā)現(xiàn)到程序執(zhí)行的某一處只出現(xiàn)過一個值,因此一個優(yōu)化的編譯器可以跳過一般情況的代碼。
然而,應(yīng)該有一個守護來檢查一般情況,如果它發(fā)生了,則此時可以編譯一般的代碼。

3.5 棧上替換

在包括Java和微軟的CLI等大多數(shù)高級語言虛擬機中,棧是指令執(zhí)行的核心。
因此,當進行優(yōu)化時,棧也是一個重要的考慮因素。

為了理解棧和程序優(yōu)化之間的關(guān)系,應(yīng)該對結(jié)構(gòu)棧實現(xiàn)棧加以區(qū)別,
結(jié)構(gòu)棧是諸如有Java或者MSIL程序指定的棧,
實現(xiàn)棧是程序執(zhí)行中(在編譯和/或優(yōu)化之后)實際使用的棧。

例如,在方法內(nèi)聯(lián)之后,實際的實現(xiàn)棧將不包含被內(nèi)聯(lián)方法的棧幀,
主調(diào)方法和被內(nèi)聯(lián)方法的棧幀被合在一起。
此外,在一些類型的優(yōu)化(如專門化)之后,實現(xiàn)棧的內(nèi)容可能和結(jié)構(gòu)棧的內(nèi)容不同。

只要結(jié)構(gòu)是正確的,那么實現(xiàn)棧和結(jié)構(gòu)棧的不同就不會帶來分歧。
結(jié)構(gòu)棧是規(guī)定程序所執(zhí)行的功能的一種手段,它不必反映實現(xiàn)功能的精確方法。

在程序執(zhí)行中的一個給定點,實現(xiàn)棧的內(nèi)容(包括幀的數(shù)量和每個幀中元素的數(shù)量)依賴于在程序上所進行的優(yōu)化。
此外,在有些情況下,動態(tài)優(yōu)化可能需要即使修改實現(xiàn)棧的內(nèi)容。
這種通過修改棧來適應(yīng)動態(tài)改變優(yōu)化級別的過程,稱為棧上替換,或者OSR(on-stack replacement)。

3.6 堆分配對象的優(yōu)化

根據(jù)其本性,好的面向?qū)ο缶幊套杂傻膭?chuàng)建大量的堆分配對象。
然而,有許多與對象相關(guān)聯(lián)的開銷。
創(chuàng)建對象和垃圾收集的代價相對較高。

此外,由于訪問保存在對象中的域經(jīng)常涉及幾層間接地址,
所以每個對象域的訪問都要經(jīng)歷小的合計開銷。

為了處理創(chuàng)建開銷,如果剖析表明一個特殊類型的對象常被分配,
那么就可以內(nèi)聯(lián)堆分配和對象初始化代碼。

標量替換是一種在某些情況下可以非常高效的降低對象訪問延遲的優(yōu)化。
這種優(yōu)化用一個標量值替換一個對象域。

當標量替換被應(yīng)用于對象時需要引用逃逸分析
這是一種確保所有對這個對象的引用在包含優(yōu)化的代碼區(qū)域內(nèi)。

在一個對象內(nèi)數(shù)據(jù)域的物理放置是一種與實現(xiàn)有關(guān)的特性。
因此,可以根據(jù)使用模式排列對象域來實現(xiàn)數(shù)據(jù)cache性能的改善。
此外,通過使用傳統(tǒng)的編譯器優(yōu)化可以把某些域引用完全刪除。

3.7 低級別的優(yōu)化

除了前面給出的面向?qū)ο蟪绦蛱貏e有效的優(yōu)化以外,還可以應(yīng)用許多傳統(tǒng)優(yōu)化。
這些優(yōu)化包括無用代碼刪除,分支優(yōu)化,復(fù)制和常數(shù)傳播,強度削弱,以及代碼重排。

在面向?qū)ο蟾呒壵Z言虛擬機中,一個潛在的重要開銷是需要進行數(shù)組范圍和空引用的檢查。
理論上,每次訪問一個數(shù)組或者使用一個引用時,就必須執(zhí)行這些檢查,或許重要的是可能會拋出一個異常。
這意味著有兩個可能的性能損失原因。

一個是本身需要執(zhí)行范圍/空檢查,另一個是禁止其他優(yōu)化,
這是因為如果檢查失敗就可能會拋出異常。
在后一種情況下,當發(fā)生異常時,結(jié)構(gòu)進程狀態(tài)必須是精確的。
并且,恰好和使用二進制優(yōu)化器一樣,如果一個精確狀態(tài)必須被潛在的具體化,那么可能會禁止某些優(yōu)化。

通過一個“不合法的”在范圍之外的地址代表空指針值,
即用一個Java進程沒有讀寫權(quán)限的內(nèi)存地址,可以大量的消除空指針檢查的開銷。
任何一個使用空指針的企圖都會導(dǎo)致一個陷阱,它通過操作系統(tǒng)支持的信號機制報告給JVM運行時系統(tǒng)。

不過異常的可能性(和需要恢復(fù)一個精確狀態(tài))仍保持不變,
所以仍要禁止某些涉及代碼移動的代碼優(yōu)化。

3.8 優(yōu)化垃圾收集

垃圾收集是高性能虛擬機實現(xiàn)的一個關(guān)鍵部分,并且編譯器可以提供許多方法幫助Java運行時系統(tǒng)提高垃圾收集的效率。

首先,有些時候堆的狀態(tài)會暫時不一致,例如,當對象引用正被修改時。
在這些點上,為了避免錯誤,不應(yīng)該啟動垃圾收集。

因此,編譯器可以在代碼中的規(guī)則間隔內(nèi)向垃圾收集器提供“退讓(yield)點”。
在這些點上線程可以保證一致的堆狀態(tài),以便控制可以被退讓給垃圾收集器。

同樣,編譯器可以幫助特定的垃圾收集算法。
例如,如果使用分代收集器,那么編譯器必須提供寫路障。


參考

虛擬機 : 系統(tǒng)與進程的通用平臺

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容