本文將是JVM 性能優(yōu)化系列的第二篇文章(第一篇:傳送門),Java 編譯器將是本文討論的核心內(nèi)容。
本文中,作者(Eva Andreasson)首先介紹了不同種類的編譯器,并對客戶端編譯,服務(wù)器端編譯器和多層編譯的運行性能進行了對比。然后,在文章的最后介紹了幾種常見的JVM優(yōu)化方法,如死代碼消除,代碼嵌入以及循環(huán)體優(yōu)化。
Java最引以為豪的特性“平臺獨立性”正是源于Java編譯器。軟件開發(fā)人員盡其所能寫出最好的java應(yīng)用程序,緊接著后臺運行的編譯器產(chǎn)生高效的基于目標(biāo)平臺的可執(zhí)行代碼。不同的編譯器適用于不同的應(yīng)用需求,因而也就產(chǎn)生不同的優(yōu)化結(jié)果。因此,如果你能更好的理解編譯器的工作原理、了解更多種類的編譯器,那么你就能更好的優(yōu)化你的Java程序。
本篇文章突出強調(diào)和解釋了各種Java虛擬機編譯器之間的不同。同時,我也會探討一些及時編譯器(JIT)常用的優(yōu)化方案。
什么是編譯器?
簡單來說,編譯器就是以某種編程語言程序作為輸入,然后以另一種可執(zhí)行語言程序作為輸出。Javac是最常見的一種編譯器。它存在于所有的JDK里面。Javac 以java代碼作為輸出,將其轉(zhuǎn)換成JVM可執(zhí)行的代碼—字節(jié)碼。這些字節(jié)碼存儲在以.class結(jié)尾的文件中,并在java程序啟動時裝載到j(luò)ava運行時環(huán)境。
字節(jié)碼并不能直接被CPU讀取,它還需要被翻譯成當(dāng)前平臺所能理解的機器指令語言。JVM中還有另一個編譯器負責(zé)將字節(jié)碼翻譯成目標(biāo)平臺可執(zhí)行的指令。一些JVM編譯器需要經(jīng)過幾個等級的字節(jié)碼代碼階段。例如,一個編譯器在將字節(jié)碼翻譯成機器指令之前可能還需要經(jīng)歷幾種不同形式的中間階段。
從平臺不可知論的角度出發(fā),我們希望我們的代碼能夠盡可能的與平臺無關(guān)。
為了達到這個目的,我們在最后一個等級的翻譯—從最低的字節(jié)碼表示到真正的機器代碼—才真正將可執(zhí)行代碼與一個特定平臺的體系結(jié)構(gòu)綁定。從最高的等級來劃分,我們可以將編譯器分為靜態(tài)編譯器和動態(tài)編譯器。 我們可以根據(jù)我們的目標(biāo)執(zhí)行環(huán)境、我們渴望的優(yōu)化結(jié)果、以及我們需要滿足的資源限制條件來選擇合適的編譯器。在上一篇文章中我們簡單的討論了一下靜態(tài)編譯器和動態(tài)編譯器,在接下來的部分我們將更加深入的解釋它們。
靜態(tài)編譯 VS 動態(tài)編譯
我們前面提到的javac就是一個靜態(tài)編譯的例子。對于靜態(tài)編譯器,輸入代碼被解釋一次,輸出即為程序?qū)肀粓?zhí)行的形式。除非你更新源代碼并(通過編譯器)重新編譯,否則程序的執(zhí)行結(jié)果將永遠不會改變:這是因為輸入是一個靜態(tài)的輸入并且編譯器是一個靜態(tài)的編譯器。
通過靜態(tài)編譯,下面的程序:
staticint add7(int x ){ return x+7;}
將會轉(zhuǎn)換成類似下面的字節(jié)碼:
iload0 bipush 7 iadd ireturn
動態(tài)編譯器動態(tài)的將一種語言編譯成另外一種語言,所謂動態(tài)的是指在程序運行的時候進行編譯—邊運行邊編譯!動態(tài)編譯和優(yōu)化的好處就是可以處理應(yīng)用程序加載時的一些變化。Java 運行時常常運行在不可預(yù)知甚至變化的環(huán)境上,因此動態(tài)編譯非常適用于Java 運行時。大部分的JVM 使用動態(tài)編譯器,如JIT編譯器。值得注意的是,動態(tài)編譯和代碼優(yōu)化需要使用一些額外的數(shù)據(jù)結(jié)構(gòu)、線程以及CPU資源。越高級的優(yōu)化器或字節(jié)碼上下文分析器,消耗越多的資源。但是這些花銷相對于顯著的性能提升來說是微不足道的。
JVM種類以及Java的平臺獨立性
所有JVM的實現(xiàn)都有一個共同的特點就是將字節(jié)碼編譯成機器指令。一些JVM在加載應(yīng)用程序時對代碼進行解釋,并通過性能計數(shù)器來找出“熱”代碼;另一些JVM則通過編譯來實現(xiàn)。編譯的主要問題是集中需要大量的資源,但是它也能帶來更好的性能優(yōu)化。
如果你是一個java新手,JVM的錯綜復(fù)雜肯定會搞得你暈頭轉(zhuǎn)向。但好消息是你并不需要將它搞得特別清楚!JVM將管理代碼的編譯和優(yōu)化,你并不需要為機器指令以及采取什么樣的方式寫代碼才能最佳的匹配程序運行平臺的體系結(jié)構(gòu)而操心。
從java字節(jié)碼到可執(zhí)行
一旦將你的java代碼編譯成字節(jié)碼,接下來的一步就是將字節(jié)碼指令翻譯成機器代碼。這一步可以通過解釋器來實現(xiàn),也可以通過編譯器來實現(xiàn)。
解釋
解釋是編譯字節(jié)碼最簡單的方式。解釋器以查表的形式找到每條字節(jié)碼指令對應(yīng)的硬件指令,然后將它發(fā)送給CPU執(zhí)行。
你可以將解釋器想象成查字典:每一個特定的單詞(字節(jié)碼指令),都有一個具體的翻譯(機器代碼指令)與之對應(yīng)。因為解釋器每讀一條指令就會馬上執(zhí)行該指令,所以該方式無法對一組指令集進行優(yōu)化。同時每調(diào)用一個字節(jié)碼都要馬上對其進行解釋,因此解釋器運行速度是相當(dāng)慢得。解釋器以一種非常準(zhǔn)確的方式來執(zhí)行代碼,但是由于沒有對輸出的指令集進行優(yōu)化,因此它對目標(biāo)平臺的處理器來說可能不是最優(yōu)的結(jié)果。
編譯
編譯器則是將所有將要執(zhí)行的代碼全部裝載到運行時。這樣當(dāng)它翻譯字節(jié)碼時,就可以參考全部或部分的運行時上下文。它做出的決定都是基于對代碼圖分析的結(jié)果。如比較不同的執(zhí)行分支以及參考運行時上下文數(shù)據(jù)。
在將字節(jié)碼序列被翻譯成機器代碼指令集后,就可以基于這個機器代碼指令集進行優(yōu)化。優(yōu)化過的指令集存儲在一個叫代碼緩沖區(qū)的結(jié)構(gòu)中。當(dāng)再次執(zhí)行這些字節(jié)碼時,就可以直接從這個代碼緩沖區(qū)中取得優(yōu)化過的代碼并執(zhí)行。在有些情況下編譯器并不使用優(yōu)化器來進行代碼優(yōu)化,而是使用一種新的優(yōu)化序列—“性能計數(shù)”。
使用代碼緩存器的優(yōu)點是結(jié)果集指令可以被立即執(zhí)行而不再需要重新解釋或編譯!
這可以大大的降低執(zhí)行時間,尤其是對一個方法被多次調(diào)用的java應(yīng)用程序。
優(yōu)化
通過動態(tài)編譯的引入,我們就有機會來插入性能計數(shù)器。例如,編譯器插入性能計數(shù)器,每次字節(jié)碼塊(對應(yīng)某個具體的方法)被調(diào)用時對應(yīng)的計數(shù)器就加一。編譯器通過這些計數(shù)器找到“熱塊”,從而就能確定哪些代碼塊的優(yōu)化能對應(yīng)用程序帶來最大的性能提升。運行時性能分析數(shù)據(jù)能夠幫助編譯器在聯(lián)機狀態(tài)下得到更多的優(yōu)化決策,從而更進一步提升代碼執(zhí)行效率。因為得到越多越精確的代碼性能分析數(shù)據(jù),我們就可以找到更多的可優(yōu)化點從而做出更好的優(yōu)化決定,例如:怎樣更好的序列話指令、是否用更有效率的指令集來替代原有指令集,以及是否消除冗余的操作等。
例如
考慮下面的java代碼
staticint add7(int x ){ return x+7;}
Javac 將靜態(tài)的將它翻譯成如下字節(jié)碼:
iload0
bipush 7
iadd
ireturn
當(dāng)該方法被調(diào)用時,該字節(jié)碼將被動態(tài)的編譯成機器指令。當(dāng)性能計數(shù)器(如果存在)達到指定的閥值時,該方法就可能被優(yōu)化。優(yōu)化后的結(jié)果可能類似下面的機器指令集:
lea rax,[rdx+7] ret
不同的編譯器適用于不同的應(yīng)用
不同的應(yīng)用程序擁有不同的需求。企業(yè)服務(wù)器端應(yīng)用通常需要長時間運行,所以通常希望對其進行更多的性能優(yōu)化;而客戶端小程序可能希望更快的響應(yīng)時間和更少的資源消耗。下面讓我們一起討論三種不同的編譯器以及他們的優(yōu)缺點。
客戶端編譯器(Client-side compilers)
C1是一種大家熟知的優(yōu)化編譯器。當(dāng)啟動JVM時,添加-client參數(shù)即可啟動該編譯器。通過它的名字我們即可發(fā)現(xiàn)C1是一種客戶端編譯器。它非常適用于那種系統(tǒng)可用資源很少或要求能快速啟動的客戶端應(yīng)用程序。C1通過使用性能計數(shù)器來進行代碼優(yōu)化。這是一種方式簡單,且對源代碼干預(yù)較少的優(yōu)化方式。
服務(wù)器端編譯器(Server-side compilers)
對于那種長時間運行的應(yīng)用程序(例如服務(wù)器端企業(yè)級應(yīng)用程序),使用客戶端編譯器可能遠遠不能夠滿足需求。這時我們應(yīng)該選擇類似C2這樣的服務(wù)器端編譯器。通過在JVM啟動行中加入 –server 即可啟動該優(yōu)化器。因為大部分的服務(wù)器端應(yīng)用程序通常都是長時間運行的,與那些短時間運行、輕量級的客戶端應(yīng)用相比,通過使用C2編譯器,你將能夠收集到更多的性能優(yōu)化數(shù)據(jù)。因此你也將能夠應(yīng)用更高級的優(yōu)化技術(shù)和算法。
提示:預(yù)熱你的服務(wù)端編譯器
對于服務(wù)器端的部署,編譯器可能需要一些時間來優(yōu)化那些“熱點”代碼。所以服務(wù)器端的部署常常需要一個“加熱”階段。所以當(dāng)對服務(wù)器端的部署進行性能測量時,務(wù)必確保你的應(yīng)用程序已經(jīng)達到了穩(wěn)定狀態(tài)!給予編譯器充足的時間進行編譯將會給你的應(yīng)用帶來很多好處。
服務(wù)器端編譯器相比客戶端編譯器來說能夠得到更多的性能調(diào)優(yōu)數(shù)據(jù),這樣就可以進行更復(fù)雜的分支分析,從而找到性能更優(yōu)的優(yōu)化路徑。擁有越多的性能分析數(shù)據(jù)就能得到更優(yōu)的應(yīng)用程序分析結(jié)果。當(dāng)然,進行大量的性能分析也就需要更多的編譯器資源。如JVM若使用C2編譯器,那么它將需要使用更多的CPU周期,更大的代碼緩存區(qū)等等。
多層編譯
多層編譯混合了客戶端編譯和服務(wù)器端編譯。Azul第一個在他的Zing JVM中實現(xiàn)了多層編譯。最近,這項技術(shù)已經(jīng)被Oracle Java Hotspot JVM采用(Java SE7 之后)。多層編譯綜合了客戶端和服務(wù)器端編譯器的優(yōu)點。客戶端編譯器在以下兩種情況表現(xiàn)得比較活躍:應(yīng)用啟動時;當(dāng)性能計數(shù)器達到較低級別的閾值時進行性能優(yōu)化。客戶端編譯器也會插入性能計數(shù)器以及準(zhǔn)備指令集以備接下來的高級優(yōu)化—服務(wù)器端編譯器—使用。多層編譯是一種資源利用率很高的性能分析方式。因為它可以在低影響編譯器活動時收集數(shù)據(jù),而這些數(shù)據(jù)可以在后面更高級的優(yōu)化中繼續(xù)使用。這種方式與使用解釋性代碼分析計數(shù)器相比可以提供更多的信息。
圖1所描述的是解釋器、客戶端編譯、服務(wù)器端編譯、多層編譯的性能比較。X軸是執(zhí)行時間(時間單位),Y軸是性能(單位時間內(nèi)的操作數(shù))
圖1.編譯器性能比較
相對于純解釋性代碼,使用客戶端編譯器可以帶來5到10倍的性能提升。獲得性能提升的多少取決于編譯器的效率、可用的優(yōu)化器種類以及應(yīng)用程序的設(shè)計與目標(biāo)平臺的吻合程度。但對應(yīng)程序開發(fā)人員來講最后一條往往可以忽略。
相對于客戶端編譯器,服務(wù)器端編譯器往往能帶來30%到50%的性能提升。在大多數(shù)情況下,性能的提升往往是以資源的損耗為代價的。
多層編譯綜合了兩種編譯器的優(yōu)點。客戶端編譯有更短的啟動時間以及可以進行快速優(yōu)化;服務(wù)器端編譯則可以在接下來的執(zhí)行過程中進行更高級的優(yōu)化操作。
一些常見的編譯器優(yōu)化
到目前為止,我們已經(jīng)討論了優(yōu)化代碼的意義以及怎樣、何時JVM會進行代碼優(yōu)化。接下來我將以介紹一些編譯器實際用到的優(yōu)化方式來結(jié)束本文。JVM優(yōu)化實際發(fā)生在字節(jié)碼階段(或者更底層的語言表示階段),但是這里將使用java語言來說明這些優(yōu)化方式。我們不可能在本節(jié)覆蓋所有的JVM優(yōu)化方式;當(dāng)然啦,我希望通過這些介紹能激發(fā)你去學(xué)習(xí)數(shù)以百計的更高級的優(yōu)化方式的興趣并在編譯器技術(shù)方面有所創(chuàng)新。
死代碼消除
死代碼消除,顧名思義就是消除那些永遠不會被執(zhí)行到的代碼—即“死”代碼。
如果編譯器在運行過程中發(fā)現(xiàn)一些多余指令,它將會將這些指令從執(zhí)行指令集里面移除。例如,在列表1里面,其中一個變量在對其進行賦值操作后永遠不會被用到,所有在執(zhí)行階段可以完全地忽略該賦值語句。對應(yīng)到字節(jié)碼級別的操作即是,永遠不需要將該變量值加載到寄存器中。不用加載意味著消耗更少的cpu時間,因此也就能加快代碼執(zhí)行,最終導(dǎo)致應(yīng)用程序加快—如果該加載代碼每秒被調(diào)用好多次,那優(yōu)化效果將更明顯。
列表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;
}
冗余消除是一種類似移除重復(fù)指令來改進應(yīng)用性能的優(yōu)化方式。
很多優(yōu)化嘗試著消除機器指令級別的跳轉(zhuǎn)指令(如 x86體系結(jié)構(gòu)中得JMP). 跳轉(zhuǎn)指令將改變指令指針寄存器,從而轉(zhuǎn)移程序執(zhí)行流。這種跳轉(zhuǎn)指令相對其他ASSEMBLY指令來說是一種很耗資源的命令。這就是為什么我們要減少或消除這種指令。代碼嵌入就是一種很實用、很有名的消除轉(zhuǎn)移指令的優(yōu)化方式。因為執(zhí)行跳轉(zhuǎn)指令代價很高,所以將一些被頻繁調(diào)用的小方法嵌入到函數(shù)體內(nèi)將會帶來很多益處。列表3-5證明了內(nèi)嵌的好處。
列表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) return0; else return x -1;}
列表5. 內(nèi)嵌方法
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中我們可以看到,一個小方法在另一個方法體內(nèi)被調(diào)用了三次,而我們想說明的是:將被調(diào)用方法直接內(nèi)嵌到代碼中所花費的代價將小于執(zhí)行三次跳轉(zhuǎn)指令所花費的代價。
內(nèi)嵌一個不常被調(diào)用的方法可能并不會帶來太大的不同,但是如果內(nèi)嵌一個所謂的“熱”方法(經(jīng)常被調(diào)用的方法)則可以帶來很多的性能提升。內(nèi)嵌后的代碼常常還可以進行更進一步的優(yōu)化,如列表6所示。
列表6. 代碼內(nèi)嵌后,更進一步的優(yōu)化實現(xiàn)
int whenToEvaluateZing(int y){ if(y ==0)return y; elseif(y ==-1)return y -1; elsereturn y + y -1;}
循環(huán)優(yōu)化
循環(huán)優(yōu)化在降低執(zhí)行循環(huán)體所帶來的額外消耗方面起著很重要的作用。這里的額外消耗指的是昂貴的跳轉(zhuǎn)、大量的條件檢測,非優(yōu)化管道(即,一系列無實際操作、消耗額外cpu周期的指令集)。這里有很多種循環(huán)優(yōu)化,接下來列舉一些比較流行的循環(huán)優(yōu)化:
循環(huán)體合并:當(dāng)兩個相鄰的循環(huán)體執(zhí)行相同次數(shù)的循環(huán)時,編譯器將試圖合并這兩個循環(huán)體。如果兩個循環(huán)體相互之間是完全獨立的,則它們還可以被同時執(zhí)行(并行)。
反演循環(huán): 最基本的,你用一個do-while循環(huán)來替代一個while循環(huán)。這個do-while循環(huán)被放置在一個if語句中。這個替換將減少兩次跳轉(zhuǎn)操作;但增加了條件判斷,因此增加了代碼量。這種優(yōu)化是以適當(dāng)?shù)脑黾淤Y源消耗換來更有效的代碼的很棒的例子—編譯器對花費和收益進行衡量,在運行時動態(tài)的做出決定。
重組循環(huán)體: 重組循環(huán)體,使整個循環(huán)體能全部的存儲在緩存器中。
展開循環(huán)體: 減少循環(huán)條件的檢測次數(shù)和跳轉(zhuǎn)次數(shù)。你可以把這想象成將幾次迭代“內(nèi)嵌”執(zhí)行,而不必進行條件檢測。循環(huán)體展開也會帶來一定的風(fēng)險,因為它可能因為影響流水線和大量的冗余指令提取而降低性能。再一次,是否展開循環(huán)體由編譯器在運行時決定,如果能帶來更大的性能提升則值得展開。
以上就是對編譯器在字節(jié)碼級別(或更低級別)如何改進應(yīng)用程序在目標(biāo)平臺執(zhí)行性能的一個概述。我們所討論的都是些常見、流行的優(yōu)化方式。由于篇幅有限我們只舉了一些簡單的例子。我們的目的是希望通過上面簡單的討論來激起你深入研究優(yōu)化的興趣。
結(jié)論:反思點和重點
根據(jù)不同的目的,選擇不同的編譯器。
1.解釋器是將字節(jié)碼翻譯成機器指令的最簡單形式。它的實現(xiàn)基于一個指令查詢表。
2.編譯器可以基于性能計數(shù)器進行優(yōu)化,但是需要消耗一些額外的資源(代碼緩存,優(yōu)化線程等)。
3.客戶端編譯器相對于解釋器可以帶來5到10倍的性能提升。
4.服務(wù)器端編譯器相對于客戶端編譯器來說可以帶來30%到50%的性能提升,但需要消耗更多的資源。
5.多層編譯則綜合了兩者的優(yōu)點。使用客戶端編譯來獲取更快的響應(yīng)速度,接著使用服務(wù)器端編譯器來優(yōu)化那些被頻繁調(diào)用的代碼。
這里有很多種可能的代碼優(yōu)化方式。編譯器的一個重要工作就是分析所有可能的優(yōu)化方式,然后對各種優(yōu)化方式所付出的代價與最終得到的機器指令帶來的性能提升進行權(quán)衡。