162
阿裏雲
技術社區[雲棲]
JVM優化係列-第二部分-編譯器
JVM性能優化係列中,第二篇章裏Java編譯器是主要部分。Eva Andreasson介紹了不同類型的編譯器,並且就客戶端、服務器端、分層編譯進行了性能對比。她也總結了JVM優化的一些概況,例如死代碼的消除、代碼內嵌和循環優化。
Java編譯器是Java著名的跨平台特性的根源。軟件開發者寫出了一個理想的Java應用程序,在編寫高效、平穩的代碼的背後,正是編譯器可以保證其運行在潛在的目標平台。不同種類的編譯器可以滿足多種應用程序的需要,產生出具體期望的性能測試結果。你對編譯器了解的越多,例如工作原理和可用種類,那麼你將更具備Java應用程序調優的能力。
JVM調優係列的第二篇文章展示並說明了各種虛擬機編譯器的不同點。同時,我也會討論使用Just-In-Time (JIT) 編譯器可以實現的共同的優化點。(JVM總攬和介紹請看本係列第一部分”JVM性能調優PART1″)
編譯器是什麼?
簡而言之,編譯器就是將編程語言作為一種輸入,然後生產出一種可執行語言。大家都知道這個工具便是javac了,所有標準JDK都包含它。 javac將Java代碼作為輸入,將其翻譯為字節碼(JVM可執行語言)。字節碼存儲在”.class”文件之中,當java程序啟動時,這些文件將被載入到java運行時環境。
字節碼不能直接被標準CPU讀取,它需要被轉譯為指令語言,以便底層的執行平台能夠解讀。JVM中負責將字節碼轉譯為可執行平台命令的組件是另外一個編譯器。有些JVM編譯器可以處理多個級別的轉譯,例如,有一個編譯器,在字節碼最終被轉譯為實際執行機器的命令之前,可以創建不同級別的字節碼中間表示形式。
字節碼和JVM
如果讀者想了解更多字節碼知識,請閱讀Bytecode basics” (Bill Venners, JavaWorld)。
就一個平台來說,從不可知觀點看,我們希望盡可能的保持代碼的平台獨立化,以便於最後一層的轉譯處理——從最低級的表達到真正的機器代碼——可以作為在具體某平台處理器構架上執行的有力控製點。最高級別的層級劃分應該是在靜態與動態編譯器之間。這樣,我們就可以根據執行環境、期望的性能測試結果、有限的資源作出我們的選擇。在Part1我已經簡要的論述了靜態和動態編譯器。接下來的部分,我將會解釋更多內容。
靜態編譯 vs 動態編譯
靜態編譯器的典型案例就是之前提到過的javac。通過靜態編譯器,輸入的代碼會被進行一次解析,輸出的可執行格式文件在程序執行的時候會被使用。除非你改變源程序並且重新編譯代碼,不然之前所輸出的代碼的執行結果將不會被改變。因為此類輸入是一種靜態輸入,並且編譯器是靜態編譯器。
靜態編譯,以下是Java代碼
將產生出一些相似的字節碼:
動態編譯器可以動態的將一種語言轉譯為另一種語言,這意味著轉譯的過程伴隨著代碼的執行,在運行時!動態編譯和優化給運行時帶來了一個優勢,即在應用載入中,運行時環境能夠及時對變化作出調整。動態編譯器十分適合Java運行時,因為它都是在難以預測並且千變萬化的環境下執行的。大部分JVM使用動態編譯器,例如JIT編譯器。關鍵是動態編譯器和代碼優化有時需要額外的數據結構、線程和CPU資源。優化或字節碼分析操作越優異,編譯過程開銷的資源就越多。因此在大部分環境中,和字節碼的重要性能收益相比,動態編譯器和代碼優化的開銷並不算大。
JVM變量和JAVA平台獨立性
所有JVM的實現都具備同一個原則,那就是要把應用的字節碼轉譯為機器指令。有些JVM在加載的時候解析應用代碼,並使用性能計數器來關注“熱點代碼”(頻繁使用的代碼)。有些JVM跳過解析環節,並隻依賴於編譯環節。這樣的話,編譯資源的密集性可能會成為個更大的問題(特別是對客戶端應用),但是這仍然支持了更多的高級優化。
如果你是個Java初學者,JVM的錯綜複雜將會讓你十分困擾。好消息是其實你根本就沒必要困擾!JVM掌管著代碼編譯和優化,因此你不需要擔心機器命令,以及編寫對底層平台構架的代碼的優化。
Java字節碼的隻讀存儲執行(校對:從Java字節碼到執行)
一旦你將java代碼編譯成字節碼,下一步將是把字節碼指令轉譯為機器指令。這可以由一個解析器或者編譯器完成。
解析
最簡單的字節碼編譯形式稱為解析。解析器隻是簡單地為每個字節碼指令查找對應的硬件指令,並將其發送至CPU去執行。
你可以將解析比作使用字典:每個具體的單詞(字節碼指令)都有一個準確的翻譯(機器代碼指令)。自從解析器讀取並即刻執行了一個字節碼指令那一刻,就已經沒有機會去優化指令集了。解析器同樣,在每次字節碼被讀取時,不得不執行解析操作,並且這個過程是十分緩慢的。解析是一種能夠執行代碼的精確方法,但是這種不可優化的輸出指令集,對目標平台處理器來說,可能不會得到最高性能的指令序列。
編譯
另一方麵,編譯器將全部要執行的代碼載入到運行時環境。當它轉譯字節碼的時候,它還具備檢查整體或部分運行時上下文的能力,並且判斷如何準確的轉譯這些代碼。它的判斷是基於代碼圖的分析,例如不同的指令執行分支和運行時上下文數據。
當一個字節碼序列被轉譯為機器指令集,並且可以對指令集進行優化時,用以替換的指令集(優化過的)被存儲進一個叫做代碼緩存的結構中。下一次這些字節碼再被執行,之前優化過的代碼可以被快速的從代碼緩存中定位到,並且用於執行。有些情況下,性能計數器可能去除並重寫之前的優化,即編譯器運行了新的優化。代碼緩存的優點是,結果指令集可以被即刻執行——不需要解析查找或者編譯!這樣就提高了執行速度,特別是對於java應用,同樣的方法要被調用很多次。
優化
使用動態編譯器可以帶來插入性能計數器的機會。比如,編譯器可能會插入一個性能計數器,在每次字節碼代碼塊被調用時,進行計算。編譯器使用“熱點代碼”相關的數據,來決定在正在運行的應用程序中,哪部分的代碼優化給程序最好的影響。運行時的切麵數據使編譯器製作出一些正在運行、或長遠提高性能的代碼優化方案。隨著更多的提煉的代碼切麵數據有效利用,它可以用於更多更好的優化決策,例如,如何在語言編譯中,獲得更好的輸出指令,是否需要替換為更有效的指令集,甚至是否需要去除多餘的操作。
參考如下代碼
以下是使用javac靜態編譯後的指令
當這個方法被調用時,字節碼塊將被動態地編譯為機器指令。當性能計數器(如果監控了此段代碼)觸發門限時,它也可能被優化。最終結果可能和以下機器指令集相似:
不同應用程序的不同編譯器
不同的Java應用具有不同需求。需要長期運行的企業級服務器端應用程序需要更多的優化,相對來說小型一些的客戶端應用程序也許需要以最小得資源開銷快速執行。讓我們看看三種不同編譯器套件和他們各自的好處於不足。
客戶端編譯器
C1是一個著名的優化編譯器,可以通過JVM啟動選-client來生效。正如它的啟動名稱一樣,C1是個客戶端編譯器。它被設計為幫助客戶端應用使用更少的資源,並且在很多情況下,它對應用程序啟動時間有所感知。C1使用性能計數器對代碼現狀切麵進行簡單的、毫無侵入性的優化。
服務器端編譯器
對於長期運行的應用程序,例如企業級服務器段Java程序來說,一個客戶端編譯器可能不夠用。服務器端編譯器例如C2可以拍上用場了。通常C2是通過JVM啟動命令-server來生效的。由於大部分服務器端程序需要長時間的運行,啟動了C2,和短時間運行的輕量級客戶端應用相比,用戶可以收集更多的切麵數據。因此,用戶可以實施更高級的優化技術和算法。
貼士:為你的服務器端編譯器熱身
由於服務器端部署的應用在編譯器對一些初始”熱點”代碼產生優化之前會消耗一些時間,因此服務器端通常需要一個”熱身”環節。在服務器端開始實施一些性能度量之前,請確保你的應用程序已經到達平穩的運行狀態!從而給編譯器足夠的時間進行適當的編譯,來為你創造收益。(想了解更多編譯器熱身和切麵原理,請看JavaWorld文章”Watch your HotSpot compiler go“)
服務器編譯器比客戶端編譯器解讀更多的切麵數據(校對:分析數據),並且允許更複雜的分支分析,這意味著它能夠評估出哪個優化方法更有效。擁有更多的切麵數據(校對:分析數據)可以產生更好的結果。當然,實施更大範圍的切麵(校對:分析)和分析(校對:計算)需要擴展編譯器的使用資源。使用了C2的JVM將使用更多的線程和CPU周期,需要更大的代碼緩存等等。
分層編譯器
分層編譯結合了客戶端和服務器端編譯。Azul最早在他的Zing虛擬機中,製造了分層編譯。最近(Java SE 7)它已經被Oracle Java Hotspot JVM所采納。分層編譯吸取了客戶端和服務器端編譯的優點。客戶端編譯器在應用啟動的時期更加主動,並且通過一些較低的性能指標閥指來觸發優化處理的動作。客戶端編譯器也可以插入性能計數器,並且為更高級的優化準備指令集,將在後期階段被服務器端編譯所處理。分層編譯器是一個非常高效利用資源的切麵(校對:分析)方法,因為它可以在對編譯器活動影響極低的時候進行數據收集,在後期高級優化時可以使用。和僅僅使用代碼解析計數器相比,這個方法還會生產出更多的信息。
以下圖表描繪了純解析、客戶端、服務器端和混合編譯的性能區別。X軸代碼執行時間,Y軸代表性能。(校對:插入圖表)
和單純的代碼解析相比,使用客戶端編譯器可以提高大約5-10倍的執行性能,實際上提高了應用的性能。當然,收益的變化還是依賴於編譯器性能如何,哪些優化生效了或者被實現了,還有就是對於目標執行平台來說應用程序設計的有多好。後者是Java開發人員從來不需要擔心的。
和客戶端編譯器相比,服務器端編譯器通常能夠提升可度量的30%-50%的代碼效率。在大部分情況下,這性能的提高將平衡掉多餘的資源開銷。
分層編譯結合了兩種編譯器的優點。客戶端編譯產生了快速的啟動時間和及時的優化,服務器端編譯在執行周期的後期,可以提供更多的高級優化。
共同的編譯器優化
目前我討論了代碼優化的價值,以及如何、什麼時候JVM編譯器能優化代碼。我將通過編譯器實際的優化情況進行總結。JVM優化實際發生在字節碼級別(或在更低的語言級別),但是我將講解使用java語言的優化。在這個部分中,我不可能覆蓋所有JVM的優化,我更多的是啟發用戶進行自我探索,並且了解大量的高級優化,以及編譯器技術的發明(請看Resources)。
去除死代碼
死代碼去除就像聽上去的一樣:代碼的去除從沒有被稱為”死代碼”。如果一個編譯器在運行過程中發現了一些執行是沒有必要的,它將輕易的將這些指令從執行的指令集中刪除。例如,列表1中,被指定了確切值的變量從來沒有被使用過,並且完全可以在執行時被省略掉。在字節碼級別,這相當於不需要執行將值載入到寄存器。不做不必要載入意味著更少的CPU時間,並促進了代碼執行時間,因此特別是熱點的代碼以及一秒鍾要調用很多此的代碼,要注意此操作。
清單1 展示了java代碼中典型的沒用的變量和不需要的操作。
1 |
int timeToScaleMyApp(boolean endlessOfResources){ |
3 |
int patchByClustering =15;
|
6 |
return reArchitect + useZing;
|
在字節碼級別,如果一個值被載入,但是從不使用,那麼編譯器是可以檢測到的並可以刪除死代碼,如代碼清單2所示。沒有執行載入操作節省了CPU時間,並且也提高了程序執行速度。
清單2 優化後的相同代碼
1 |
int timeToScaleMyApp(boolean endlessOfResources){ |
3 |
//unnecessary operation removed here...
|
6 |
return reArchitect + useZing;
|
多餘性的刪除是一個相似的優化操作,它將會移除重複的指令,來提高應用的性能。
代碼嵌入
很多優化都是嚐試去除主機層的jump指令(例如x86構架的JMP指令)。jump指令改變了指令指針寄存器,因此轉變了執行流程。和其他ASSEMBLY類指令相比,這是個高成本的操作,這就是為什麼要減少或消除jump指令。一個非常有用和著名的優化就是代碼嵌入。既然jump非常昂貴,那麼這種方式能有所幫助,即在調用區間內,內嵌很多可以頻繁調用的,不同入口地址的小方法。代碼清單3-5示範了內嵌代碼的好處。
調用方法
1 |
int whenToEvaluateZing(int y){ |
2 |
return daysLeft(y)+ daysLeft(0)+ daysLeft(y+1);
|
被調用方法
內嵌方法
1 |
int whenToEvaluateZing(int y){ |
3 |
if(y ==0) temp +=0;else temp += y -1;
|
4 |
if(0==0) temp +=0;else temp +=0-1;
|
5 |
if(y+1==0) temp +=0;else temp +=(y +1)-1;
|
清單3-5中,在調用的主方法中,其調用了三次小的方法,這樣我們假設這個例子的目的是展示內嵌代碼比跳躍三次更有利。
對於一個很少調用到的方法,內嵌代碼不會產生太大的不同,但是對於頻繁調用的熱點代碼,這將意味著性能上巨大的不同。內嵌也常常對更深遠的優化有所幫助,如清單6所示。
內嵌,可以采用更多的優化
1 |
int whenToEvaluateZing(int y){ |
3 |
else if(y ==-1)return y -1;
|
循環優化
循環優化在降低執行循環代碼的開銷有著重要的作用。在這種情況下,係統開銷意味著昂貴的指針轉移、大量的條件判斷、沒有選擇(校對:優化)的指令管道(也就是說,大量的指令集會導致無操作或CPU的額外周期)。循環優化有很多種,以及大量的優化組合。典型的包括:
- 混合循環:當兩個相鄰的循環被迭代,循環次數相同,這個編譯器能夠嚐試混合循環的主體,在相同的時間被執行(並行),當然兩個循環體內部不能有相互的引用,也就是說,他們必須是完全的相互獨立。
- 反向循環:基本上你可以使用do-while循環替代while循環。因為do-while循環具備一個if子句。這個替換可以減少兩次指針轉移。然而,這也增加了條件判斷和增加了代碼數量。這種優化是一個極好的例子,即如何多付出一點資源來換取更加高效的代碼,在動態運行時,編譯器不得不評估和決定開銷和收益的平衡
- Tiling loops:重組循環,以便於迭代的數據塊尺寸適合緩存
- Unrolling loops:能降低循環條件的判斷次數和指針轉移次數。你可以將其想象為內嵌兩三個要執行的迭代體,並且不需要接觸到循環條件。unrolling loops運行有風險,因為它可能會造成管道減少和多餘的指令操作,從而降低性能。重申一下,這個判斷是編譯器運行時作出的,也就是說,如果收益足夠,那麼付出的開銷也是值得的
這就是一個概要,即在字節碼級別(或更低級別)編譯器做了些什麼來改進應用在目標平台上的執行效率。這些優化是共通而普遍的,但是隻有一些可選的短小示範。這些非常簡單而寬泛的講解也是為了激發讀者的興趣,從而進行更深度的探索。
綜上所述:反射點和亮點
為不同的需求選擇不同的編譯器
- 轉譯是一個字節碼翻譯為機器指令的最簡單形式,並且基於指令查詢表工作
- 編譯器基於性能計數器的優化,但將需要一些附加的資源開銷(代碼緩存、優化線程等)
- 和轉譯代碼相比,客戶端編譯器可以大大提高代碼執行性能(5-10倍)
- 服務器端編譯器比客戶端編譯器能提高應用性能30%-50%,但是消耗更多的資源
- 分層編譯器提供了兩者的最佳能力。具備客戶端編譯能力而提高代碼執行性能,並且服務端編譯隨時間而定,而使頻繁執行的代碼性能更好。
有很多重可行的代碼優化。對於編譯器來說一個種要的任務就是分析所有可能性,並且基於輸出的主機代碼的執行速度衡量采用優化的開銷。
最後更新:2017-05-23 18:02:19