您好,登錄后才能下訂單哦!
Java編譯器在JVM性能優(yōu)化系列的第二篇文章中占據(jù)中心位置。 Eva Andreasson介紹了不同種類的編譯器,并比較了客戶端,服務(wù)器和分層編譯的性能結(jié)果。最后,她概述了常見的JVM優(yōu)化,例如消除死代碼,內(nèi)聯(lián)和循環(huán)優(yōu)化。
Java編譯器是Java著名的平臺的獨立性的來源。軟件開發(fā)人員會盡力編寫最好的Java應(yīng)用程序,然后編譯器會在幕后進行工作,以為目標(biāo)目標(biāo)平臺生成高效且性能良好的執(zhí)行代碼。不同種類的編譯器可滿足各種應(yīng)用程序需求,從而產(chǎn)生特定的所需性能結(jié)果。你對編譯器了解得越多,就它們的工作方式和可用的種類而言,你就越能夠優(yōu)化Java應(yīng)用程序性能。
什么是編譯器?
簡而言之,編譯器將編程語言作為輸入,并產(chǎn)生可執(zhí)行語言作為輸出。 一種常見的編譯器是Javac,它包含在所有標(biāo)準(zhǔn)Java開發(fā)工具包(JDK)中。 javac將Java代碼作為輸入并將其轉(zhuǎn)換為字節(jié)碼-JVM的可執(zhí)行語言。 字節(jié)碼存儲在.class文件中,當(dāng)啟動Java進程時,該文件將加載到Java運行時中。
字節(jié)碼不能被標(biāo)準(zhǔn)CPU讀取,需要轉(zhuǎn)換為底層執(zhí)行平臺可以理解的指令語言。 JVM中負責(zé)將字節(jié)碼轉(zhuǎn)換為可執(zhí)行平臺指令的組件是另一個編譯器。 一些JVM編譯器可以處理多個級別的轉(zhuǎn)換。 例如,編譯器可能在將字節(jié)碼轉(zhuǎn)換成實際的機器指令(即翻譯的最后一步)之前,創(chuàng)建字節(jié)碼的各種中間表示形式。
從平臺不可知的角度來看,我們希望盡可能使代碼獨立于平臺,以便最后的翻譯級別(從最低表示到實際的機器代碼)是將執(zhí)行鎖定到特定平臺的處理器體系結(jié)構(gòu)的步驟 。 靜態(tài)和動態(tài)編譯器之間的最高級別隔離。 從那里開始,我們有選擇,這取決于我們要針對的執(zhí)行環(huán)境,所需的性能結(jié)果以及需要滿足的資源限制。 我在本系列的第1部分中簡要討論了靜態(tài)和動態(tài)編譯器。 在以下各節(jié)中,我將進一步解釋。
靜態(tài)與動態(tài)編譯
靜態(tài)編譯器的一個示例是前面提到的javac。 對于靜態(tài)編譯器,輸入代碼將被解釋一次,而輸出可執(zhí)行文件的形式將在執(zhí)行程序時使用。 除非你對原始源代碼進行更改并重新編譯代碼(使用編譯器),否則輸出將始終產(chǎn)生相同的結(jié)果。 這是因為輸入是靜態(tài)輸入,而編譯器是靜態(tài)編譯器。
在靜態(tài)編譯中,
static int add7( int x ) {
return x+7;}
會導(dǎo)致類似于以下字節(jié)碼的內(nèi)容:
iload0
bipush 7
iadd
ireturn
動態(tài)編譯器會動態(tài)地將一種語言翻譯成另一種語言,這意味著它會在執(zhí)行代碼時發(fā)生-在運行時! 動態(tài)編譯和優(yōu)化為運行時提供了能夠適應(yīng)應(yīng)用程序負載變化的優(yōu)勢。 動態(tài)編譯器非常適合Java運行時,這些運行時通常在不可預(yù)測且不斷變化的環(huán)境中執(zhí)行。 大多數(shù)JVM使用動態(tài)編譯器,例如即時(JIT)編譯器。 問題是動態(tài)編譯器和代碼優(yōu)化有時需要額外的數(shù)據(jù)結(jié)構(gòu),線程和CPU資源。 優(yōu)化或字節(jié)碼上下文分析越高級,編譯消耗的資源就越多。 與輸出代碼的顯著性能相比,在大多數(shù)環(huán)境中,開銷仍然很小。
JVM種類和Java平臺的獨立性
所有JVM實現(xiàn)都有一個共同點,那就是它們試圖將應(yīng)用程序字節(jié)碼轉(zhuǎn)換為機器指令。 一些JVM在加載時解釋應(yīng)用程序代碼,并使用性能計數(shù)器來關(guān)注“熱”代碼。 一些JVM跳過解釋,僅依靠編譯。 編譯的資源密集性可能會受到更大的影響(尤其是對于客戶端應(yīng)用程序),但它還可以實現(xiàn)更高級的優(yōu)化。
從Java字節(jié)碼到執(zhí)行
將Java代碼編譯為字節(jié)碼后,下一步就是將字節(jié)碼指令轉(zhuǎn)換為機器碼。 這可以由解釋器或編譯器完成。
解釋
字節(jié)碼編譯的最簡單形式稱為解釋。 解釋器只需為每個字節(jié)碼指令查找硬件指令,然后將其發(fā)送出去以由CPU執(zhí)行。
你可能會想到解釋,類似于使用字典:對于特定單詞(字節(jié)碼指令),存在確切的翻譯(機器碼指令)。 由于解釋器一次讀取并立即執(zhí)行一個字節(jié)碼指令,因此沒有機會對指令集進行優(yōu)化。 每次調(diào)用字節(jié)碼時,解釋器也必須執(zhí)行解釋,這使它相當(dāng)慢。 解釋是執(zhí)行代碼的一種準(zhǔn)確方法,但是未優(yōu)化的輸出指令集可能不是目標(biāo)平臺處理器的最高性能序列。
總結(jié)
另一方面,編譯器會將要執(zhí)行的整個代碼加載到運行時中。在翻譯字節(jié)碼時,它可以查看整個或部分運行時上下文,并就如何實際翻譯代碼做出決策。它的決策基于對代碼圖的分析,例如對指令和運行時上下文數(shù)據(jù)的不同執(zhí)行分支。
當(dāng)將字節(jié)碼序列轉(zhuǎn)換為機器碼指令集并可以對該指令集進行優(yōu)化時,替換指令集(例如優(yōu)化序列)將存儲到稱為代碼緩存的結(jié)構(gòu)中。下次執(zhí)行該字節(jié)碼時,先前優(yōu)化的代碼可以立即位于代碼緩存中,并用于執(zhí)行。在某些情況下,性能計數(shù)器可能會加入并覆蓋之前的優(yōu)化,在這種情況下,編譯器將運行新的優(yōu)化序列。代碼緩存的優(yōu)點是可以立即執(zhí)行生成的指令集-無需解釋性查找或編譯!這加快了執(zhí)行時間,尤其是對于Java方法,其中多次調(diào)用相同的方法。
優(yōu)化
除了動態(tài)編譯外,還可以插入性能計數(shù)器。 例如,編譯器可能會插入一個性能計數(shù)器,以在每次調(diào)用字節(jié)碼塊(例如對應(yīng)于特定方法)時進行計數(shù)。 編譯器使用有關(guān)給定字節(jié)碼有多“熱”的數(shù)據(jù)來確定代碼中的最優(yōu)化將對運行中的應(yīng)用程序產(chǎn)生最佳影響。 運行時概要分析數(shù)據(jù)使編譯器可以即時制定豐富的代碼優(yōu)化決策集,從而進一步提高代碼執(zhí)行性能。 隨著更完善的代碼概要分析數(shù)據(jù)的可用,它可以用于做出其他更好的優(yōu)化決策,例如:如何更好地以編譯為語言對指令進行排序,是否用更高效的指令集代替指令集,甚至 是否消除冗余操作。
例
考慮一下Java代碼:
static int add7( int x ) {
return x+7;}
這可以由javac靜態(tài)編譯為字節(jié)碼:
iload0
bipush 7
iadd
ireturn
調(diào)用該方法時,字節(jié)碼塊將動態(tài)編譯為機器指令。 當(dāng)性能計數(shù)器(如果存在于代碼塊中)達到閾值時,它可能也會得到優(yōu)化。 對于給定的執(zhí)行平臺,最終結(jié)果可能類似于以下機器指令集:
lea rax,[rdx+7]
ret
不同應(yīng)用程序的不同編譯器
不同的Java應(yīng)用程序有不同的需求。 長期運行的企業(yè)服務(wù)器端應(yīng)用程序可以進行更多優(yōu)化,而較小的客戶端應(yīng)用程序可能需要以最小的資源消耗來快速執(zhí)行。 讓我們考慮三種不同的編譯器設(shè)置及其各自的優(yōu)缺點。
客戶端編譯器
著名的優(yōu)化編譯器是C1,它是通過-client JVM啟動選項啟用的編譯器。 顧名思義,C1是客戶端編譯器。 它是為客戶端應(yīng)用程序設(shè)計的,這些客戶端應(yīng)用程序具有較少的可用資源,并且在許多情況下對應(yīng)用程序啟動時間敏感。 C1使用性能計數(shù)器進行代碼性能分析,以實現(xiàn)簡單,相對無干擾的優(yōu)化。
服務(wù)器端編譯器
對于長時間運行的應(yīng)用程序(例如服務(wù)器端企業(yè)Java應(yīng)用程序),客戶端編譯器可能不夠。 可以使用類似C2的服務(wù)器端編譯器。 通常通過在啟動命令行中添加JVM啟動選項-server來啟用C2。 由于大多數(shù)服務(wù)器端程序預(yù)計將運行很長時間,因此啟用C2意味著你將比使用運行時間短的輕量級客戶端應(yīng)用程序收集更多的性能分析數(shù)據(jù)。 因此,你將能夠應(yīng)用更高級的優(yōu)化技術(shù)和算法。
服務(wù)器編譯器比客戶端編譯器處理更多的概要分析數(shù)據(jù),并且允許進行更復(fù)雜的分支分析,這意味著它將考慮哪種優(yōu)化路徑會更有利。具有更多可用的概要分析數(shù)據(jù)可產(chǎn)生更好的應(yīng)用程序結(jié)果。當(dāng)然,進行更廣泛的分析和分析需要在編譯器上花費更多的資源。啟用了C2的JVM將使用更多的線程和更多的CPU周期,需要更大的代碼緩存,依此類推。
分層編譯
分層編譯將客戶端和服務(wù)器端編譯組合在一起。 Azul首先在其Zing JVM中提供了分層編譯。最近(從Java SE 7開始),Oracle Java Hotspot JVM已采用它。分層編譯利用了JVM中客戶端和服務(wù)器編譯器的優(yōu)勢??蛻舳司幾g器在應(yīng)用程序啟動期間最活躍,并處理由較低的性能計數(shù)器閾值觸發(fā)的優(yōu)化??蛻舳司幾g器還會插入性能計數(shù)器并為更高級的優(yōu)化準(zhǔn)備指令集,服務(wù)器端編譯器將在稍后階段解決這些問題。分層編譯是一種非常節(jié)省資源的性能分析方法,因為編譯器能夠在影響較小的編譯器活動期間收集數(shù)據(jù),以后可以將其用于更高級的優(yōu)化。與僅使用解釋的代碼配置文件計數(shù)器所獲得的信息相比,這種方法還可以產(chǎn)生更多的信息。
圖1中的圖表架構(gòu)描述了純解釋,客戶端,服務(wù)器端和分層編譯之間的性能差異。 X軸顯示執(zhí)行時間(時間單位),Y軸性能(操作數(shù)/時間單位)。
與純解釋代碼相比,使用客戶端編譯器可以使執(zhí)行性能(以ops / s為單位)提高約5至10倍,從而提高了應(yīng)用程序性能。增益的變化當(dāng)然取決于編譯器的效率,啟用或?qū)崿F(xiàn)的優(yōu)化方式以及(在較小程度上)應(yīng)用程序相對于目標(biāo)執(zhí)行平臺的良好設(shè)計。不過,后者確實是Java開發(fā)人員永遠不必擔(dān)心的事情。
與客戶端編譯器相比,服務(wù)器端編譯器通??蓪⒋a性能提高30%到50%。在大多數(shù)情況下,性能改進將平衡額外的資源成本。
分層編譯結(jié)合了兩種編譯器的最佳功能??蛻舳司幾g可縮短啟動時間并加快優(yōu)化速度,而服務(wù)器端編譯可在執(zhí)行周期后期提供更高級的優(yōu)化。
一些常見的編譯器優(yōu)化
消除無效代碼
消除無效代碼聽起來像是:消除從未被調(diào)用的代碼-即 無效 代碼。 如果編譯器在運行時發(fā)現(xiàn)某些指令是不必要的,它將僅從執(zhí)行指令集中消除它們。 例如,在清單1中,從不使用變量的特定值分配,并且可以在執(zhí)行時將其完全忽略。 在字節(jié)碼級別,這可能對應(yīng)于永遠不需要執(zhí)行將值加載到寄存器中的操作。 不必執(zhí)行加載意味著更少的CPU時間,從而縮短了代碼執(zhí)行速度,進而縮短了應(yīng)用程序的時間-尤其是當(dāng)代碼很熱并且每秒被調(diào)用幾次時。
清單1顯示了Java代碼,該代碼示例了一個從未使用過的變量,這是不必要的操作。
清單 1. 清除無效代碼
int timeToScaleMyApp(boolean endlessOfResources) {
int reArchitect = 24;
int patchByClustering = 15;
int useZing = 2;
if(endlessOfResources)
return reArchitect + useZing;
else
return useZing;}
在字節(jié)碼級別上,如果加載了一個值但從未使用過,則編譯器可以檢測到該值并消除死代碼,如清單2所示。從不執(zhí)行加載可以節(jié)省CPU時間,從而提高程序的執(zhí)行速度。
清單2.優(yōu)化后的相同代碼
int timeToScaleMyApp(boolean endlessOfResources) {
int reArchitect = 24;
//unnecessary operation removed here...
int useZing = 2;
if(endlessOfResources)
return reArchitect + useZing;
else
return useZing;}
冗余消除是類似的優(yōu)化,它刪除重復(fù)的指令以提高應(yīng)用程序性能。
內(nèi)聯(lián)
許多優(yōu)化嘗試消除機器級別的跳轉(zhuǎn)指令(例如,用于x86架構(gòu)的JMP)。 跳轉(zhuǎn)指令更改指令指針寄存器,從而傳輸執(zhí)行流程。 相對于其他ASSEMBLY指令,這是一項昂貴的操作,這就是為什么它是減少或消除的常見目標(biāo)的原因。 針對此的非常有用且眾所周知的優(yōu)化稱為內(nèi)聯(lián)。 由于跳轉(zhuǎn)很昂貴,因此將許多頻繁調(diào)用具有不同入口地址的小型方法的調(diào)用內(nèi)插會很有幫助。 清單3至5中的Java代碼體現(xiàn)了內(nèi)聯(lián)的好處。
清單3.調(diào)用者方法
int whenToEvaluateZing(int y) {
return daysLeft(y) + daysLeft(0) + daysLeft(y+1);}
清單4.調(diào)用的方法
int daysLeft(int x){
if (x == 0)
return 0;
else
return x - 1;}
清單5.內(nèi)聯(lián)方法
int whenToEvaluateZing(int y){
int temp = 0;
if(y == 0) temp += 0; else temp += y - 1;
if(0 == 0) temp += 0; else temp += 0 - 1;
if(y+1 == 0) temp += 0; else temp += (y + 1) - 1;
return temp; }
在清單3至清單5中,調(diào)用方法對一個小方法進行了3次調(diào)用,出于示例的考慮,我們認為對內(nèi)聯(lián)方法而言,跳轉(zhuǎn)到三遍更為有益。
內(nèi)聯(lián)很少調(diào)用的方法可能不會有太大的區(qū)別,但是內(nèi)聯(lián)經(jīng)常調(diào)用的所謂“熱”方法可能意味著性能上的巨大差異。 內(nèi)聯(lián)還經(jīng)常為進一步的優(yōu)化讓路,如清單6所示
清單6.內(nèi)聯(lián)之后,可以應(yīng)用更多的優(yōu)化
int whenToEvaluateZing(int y){
if(y == 0) return y;
else if (y == -1) return y - 1;
else return y + y - 1;}
循環(huán)優(yōu)化
循環(huán)優(yōu)化在減少執(zhí)行循環(huán)帶來的開銷方面發(fā)揮著重要作用。 在這種情況下,開銷意味著昂貴的跳轉(zhuǎn),條件檢查的次數(shù),非最佳指令流水線(即導(dǎo)致CPU不工作或額外循環(huán)的指令順序)。 循環(huán)優(yōu)化有很多種,總計有很多優(yōu)化。 值得注意的包括:
合并循環(huán):當(dāng)兩個附近的循環(huán)重復(fù)相同的時間時,如果主體中沒有相互引用的情況,則編譯器可以嘗試合并循環(huán)的主體,以同時(并行)執(zhí)行,即它們彼此完全獨立。
反轉(zhuǎn)循環(huán):基本上,你將常規(guī)的while循環(huán)替換為do-while循環(huán)。并且do-while循環(huán)設(shè)置為if子句。這種替換導(dǎo)致更少的兩次跳躍。但是,它增加了條件檢查,因此增加了代碼大小。此優(yōu)化是一個很好的示例,說明了如何使用更多的資源來提高代碼效率-編譯器必須在運行時動態(tài)評估和決定成本與收益的平衡。
切片循環(huán):重新組織循環(huán),以便對大小適合緩存的數(shù)據(jù)塊進行迭代。
展開循環(huán):減少必須評估循環(huán)條件的次數(shù)以及跳轉(zhuǎn)次數(shù)。你可以將其視為“內(nèi)聯(lián)”要執(zhí)行的主體的多次迭代,而不會越過循環(huán)條件。展開循環(huán)會帶來風(fēng)險,因為展開循環(huán)可能會損害流水線并導(dǎo)致多次冗余指令提取,從而降低性能。再次,這是編譯器在運行時做出的判斷調(diào)用,即,如果增益足夠,則成本可能是值得的。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。