JVM性能優化, Part 2 ―― 編譯器


ImportNew注:本文是JVM性能優化 – 第2篇 《JVM性能優化, Part 2 ―― 編譯器》第一篇 《JVM性能優化, Part 1 ―― JVM簡介 》java


做爲JVM性能優化系列文章的第2篇,本文將着重介紹Java編譯器,此外還將對JIT編譯器經常使用的一些優化措施進行討論(參見「JVM性能優化,Part 1″中對JVM的介紹)。Eva Andreasson將對不一樣種類的編譯器作介紹,並比較客戶端、服務器端和層次編譯產生的編譯結果在性能上的區別,此外將對通用的JVM優化作介紹,包括死代碼剔除、內聯以及循環優化。程序員


Java編譯器存在是Java編程語言能獨立於平臺的根本緣由。軟件開發者能夠盡全力編寫程序,而後由Java編譯器將源代碼編譯爲針對於特定平臺的高效、可運行的代碼。不一樣類型的編譯器適合於不一樣應用程序的需求,使編譯結果能夠知足指望的性能要求。對編譯器基本原理了解得越多,在優化Java應用程序性能時就越能駕輕就熟。算法


什麼是編譯器編程


簡單來講,編譯器就是將一種編程語言做爲輸入,輸出另外一種可執行語言的工具。你們都熟悉的javac就是一個編譯器,全部標準版的JDK中都帶有這個工具。javac以Java源代碼做爲輸入,將其翻譯爲可由JVM執行的字節碼。翻譯後的字節碼存儲在.class文件中,在啓動Java進程的時候,被載入到Java運行時中。緩存


標準CPU並不能識別字節碼,它須要被轉換爲當前平臺所能理解的本地指令。在JVM中,有專門的組件負責將字節碼編譯爲平臺相關指令,實際上,這也是一種編譯器。有些JVM編譯器能夠處理多層級的編譯工做,例如,編譯器在最終將字節碼轉換爲平臺相關指令前,會爲相關的字節碼創建多層級的中間表示(intermediate representation)。性能優化


字節碼與JVM服務器


若是你想了解更多有關字節碼與JVM的信息,請閱讀 「Bytecode basics」(Bill Venners, JavaWorld)數據結構


以平臺未知的角度看,咱們但願儘量的保持平臺獨立性,所以,最後一級的編譯,也就是從最低級表示到實際機器碼的轉換,是與具體平臺的處理器架構息息相關的。在最高級的表示上,會因使用靜態編譯器仍是動態編譯器而有所區別。在這裏,咱們能夠選擇應用程序因此來的可執行環境,指望達到的性能要求,以及咱們所面臨的資源限制。在本系列的第1篇文章的靜態編譯器與動態編譯器一節中,已經對此有過簡要介紹。我將在本文的後續章節中詳細介紹這部份內容。架構


靜態編譯器與動態編譯器app


前文提到的javac就是使用靜態編譯器的例子。靜態編譯器解釋輸入的源代碼,並輸出程序運行時所需的可執行文件。若是你修改了源代碼,那麼就須要使用編譯器來從新編譯代碼,不然輸出的可執行性文件不會發生變化;這是由於靜態編譯器的輸入是靜態的普通文件。


使用靜態編譯器時,下面的Java代碼


1

2

3

static int add7( int x ) {

     return x+7;

}

會生成相似以下的字節碼:


1

2

3

4

iload0

bipush 7

iadd

ireturn

動態編譯器會動態的將一種編程語言編譯爲另外一種,即在程序運行時執行編譯工做。動態編譯與優化使運行時能夠根據當前應用程序的負載狀況而作出相應的調整。動態編譯器很是適合用於Java運行時中,由於Java運行時一般運行在沒法預測而又會隨着運行而有所變更的環境中。大部分JVM都會使用諸如Just-In-Time編譯器的動態編譯器。這裏面須要注意的是,大部分動態編譯器和代碼優化有時須要使用額外的數據結構、線程和CPU資源。要作的優化或字節碼上下文分析越高級,編譯過程所消耗的資源就越多。在大多數運行環境中,相比於通過動態編譯和代碼優化所得到的性能提高,這些損耗微不足道。


 JVM的多樣性與Java平臺的獨立性


全部的JVM實現都有一個共同點,即它們都試圖將應用程序的字節碼轉換爲本地機器指令。一些JVM在載入應用程序後會解釋執行應用程序,同時使用性能計數器來查找「熱點」代碼。還有一些JVM會調用解釋執行的階段,直接編譯運行。資源密集型編譯任務對應用程序來講可能會產生較大影響,尤爲是那些客戶端模式下運行的應用程序,可是資源密集型編譯任務能夠執行一些比較高級的優化任務。更多相關內容請參見相關資源


若是你是Java初學者,JVM自己錯綜複雜結構會讓你暈頭轉向的。不過,好消息是你無需精通JVM。JVM本身會作好代碼編譯和優化的工做,因此你無需關心如何針對目標平臺架構來編寫應用程序才能編譯、優化,從而生成更好的本地機器指令。


從字節碼到可運行的程序


當你編寫完Java源代碼並將之編譯爲字節碼後,下一步就是將字節碼指令編譯爲本地機器指令。這一步會由解釋器或編譯器完成。


解釋


解釋是最簡單的字節碼編譯形式。解釋器查找每條字節碼指令對應的硬件指令,再由CPU執行相應的硬件指令。


你能夠將解釋器想象爲一個字典:每一個單詞(字節碼指令)都有準確的解釋(本地機器指令)。因爲解釋器每次讀取一個字節碼指令並當即執行,所以它就沒有機會對某個指令集合進行優化。因爲每次執行字節碼時,解釋器都須要作相應的解釋工做,所以程序運行起來就很慢。解釋執行能夠準確執行字節碼,可是未經優化而輸出的指令集難以發揮目標平臺處理器的最佳性能。


編譯


另外一方面,編譯執行應用程序時,*編譯器*會將加載運行時會用到的所有代碼。由於編譯器能夠將字節碼編譯爲本地代碼,所以它能夠獲取到完整或部分運行時上下文信息,並依據收集到的信息決定到底應該如何編譯字節碼。編譯器是根據諸如指令的不一樣執行分支和運行時上下文數據等代碼信息來指定決策的。


當字節碼序列被編譯爲機器代碼指令集合時,就能夠對這個指令集合作一些優化操做了,優化後的指令集合會被存儲到成爲code cache的數據結構中。當下一次執行這部分字節碼序列時,就會執行這些通過優化後被存儲到code cache的指令集合。在某些狀況下,性能計數器會失效,並覆蓋掉先前所作的優化,這時,編譯器會執行一次新的優化過程。使用code cache的好處是優化後的指令集能夠當即執行 —— 無需像解釋器同樣再通過查找的過程或編譯過程!這能夠加速程序運行,尤爲是像Java應用程序這種同一個方法會被屢次調用應用程序。


優化


隨着動態編譯器一塊兒出現的是性能計數器。例如,編譯器會插入性能計數器,以統計每一個字節碼塊(對應與某個被調用的方法)的調用次數。在進行相關優化時,編譯器會使用收集到的數據來判斷某個字節碼塊有多「熱」,這樣能夠最大程度的下降對當前應用程序的影響。運行時數據監控有助於編譯器完成多種代碼優化工做,進一步提高代碼執行性能。隨着收集到的運行時數據愈來愈多,編譯器就能夠完成一些額外的、更加複雜的代碼優化工做,例如編譯出更高質量的目標代碼,使用運行效率更高的代碼替換原代碼,甚至是剔除冗餘操做等。


示例


考慮以下代碼:


1

2

3

static int add7( int x ) {

     return x+7;

}

這段代碼通過javac編譯後會產生以下的字節碼:


1

2

3

4

iload0

bipush 7

iadd

ireturn

當調用這段代碼時,字節碼塊會被動態的編譯爲本地機器指令。當性能計數器(若是這段代碼應用了性能計數器的話)發現這段代碼的運行次數超過了某個閾值後,動態編譯器會對這段代碼進行優化編譯。後帶的代碼可能會是下面這個樣子:


1

2

lea rax,[rdx+7]

ret

各擅勝場


不一樣的Java應用程序須要知足不一樣的需求。相對來講,企業級服務器端應用程序須要長時間運行,所以能夠作更多的優化,而稍小點的客戶端應用程序可能要求快速啓動運行,佔資源少。接下來咱們考察三種編譯器設置及其各自的優缺點。


客戶端編譯器


即你們熟知的優化編譯器C1。在啓動應用程序時,添加JVM啓動參數「-client」能夠啓用C1編譯器。正如啓動參數所表示的,C1是一個客戶端編譯器,它專爲客戶端應用程序而設計,資源消耗更少,而且在大多數狀況下,對應用程序的啓動時間很敏感。C1編譯器使用性能計數器來收集代碼的運行時信息,執行一些簡單、無侵入的代碼優化任務。


服務器端編譯器


對於那些須要長時間運行的應用程序,例如服務器端的企業級Java應用程序來講,客戶端編譯器所實現的功能還略有不足,所以服務器端的編譯會使用相似C2這類的編譯器。啓動應用程序時添加命令行參數「-server」能夠啓用C2編譯器。因爲大多數服務器端應用程序都會長時間運行,所以相對於運行時間稍短的輕量級客戶端應用程序,在服務器端應用程序中啓用C2編譯器能夠收集到更多的運行時數據,也就能夠執行一些更高級的編譯技術與算法。


提示:給服務器端編譯器熱身


對於服務器端編譯器來講,在應用程序開始運行以後,編譯器可能會在一段時間以後纔開始優化「熱點」代碼,因此服務器端編譯器一般須要通過一個「熱身」階段。在服務器端編譯器執行性能優化任務以前,要確保應用程序的各項準備工做都已就緒。給予編譯器足夠多的時間來完成編譯、優化的工做才能取得更好的效果。(更多關於編譯器熱身與監控原理的內容請參見JavaWorld的文章」Watch your HotSpot compiler go「。)


在執行編譯任務優化任務時,服務器端編譯器要比客戶端編譯器綜合考慮更多的運行時信息,執行更復雜的分支分析,即對哪一種優化路徑能取得更好的效果做出判斷。獲取的運行時數據越多,編譯優化所產生的效果越好。固然,要完成一些複雜的、高級的性能分析任務,編譯器就須要消耗更多的資源。使用了C2編譯器的JVM會消耗更多的資源,例如更多的線程,更多的CPU指令週期,以及更大的code cache等。


層次編譯


層次編譯綜合了服務器端編譯器和客戶端編譯器的特色。Azul首先在其Zing JVM中實現了層次編譯。最近(就是Java SE 7版本),Oracle Java HotSpot VM也採用了這種設計。在應用程序啓動階段,客戶端編譯器最爲活躍,執行一些由較低的性能計數器閾值出發的性能優化任務。此外,客戶端編譯器還會插入性能計數器,爲一些更復雜的性能優化任務準備指令集,這些任務將在後續的階段中由服務器端編譯器完成。層次編譯能夠更有效的利用資源,由於編譯器在執行一些對應用程序影響較小的編譯活動時仍能夠繼續收集運行時信息,而這些信息能夠在未來用於完成更高級的優化任務。使用層次編譯能夠比解釋性的代碼性能計數器手機到更多的信息。


Figure 1中展現了純解釋運行、客戶端模式運行、服務器端模式運行和層次編譯模式運行下性能之間的區別。X軸表示運行時間(單位時間)Y軸表示性能(每單位時間內的操做數)。



Figure 1. Performance differences between compilers (click to enlarge)


編譯性能對比


相比於純解釋運行的的代碼,以客戶端模式編譯運行的代碼在性能(指單位時間執行的操做)上能夠達到約5到10倍,所以而提高了應用程序的運行性能。其間的區別主要在於編譯器的效率、編譯器所做的優化,以及應用程序在設計實現時針對目標平臺作了何種程度的優化。實際上,最後一條不在Java程序員的考慮之列。


相比於客戶端編譯器,使用服務器端編譯器一般會有30%到50%的性能提高。在大多數狀況下,這種程度的性能提高足以彌補使用服務器端編譯所帶來的額外資源消耗。


層次編譯綜合了服務器端編譯器和客戶端編譯器的優勢,使用客戶端編譯模式實現快速啓動和快速優化,使用服務器端編譯模式在後續的執行週期中完成高級優化的編譯任務。


經常使用編譯優化手段


到目前爲止,已經介紹了優化代碼的價值,以及經常使用JVM編譯器是如何以及什麼時候編譯代碼的。接下來,將用一些實際的例子作個總結。JVM所做的性能優化一般在字節碼這一層級(或者是更底層的語言表示),但這裏我將使用Java編程語言對優化措施進行介紹。在這一節中,我沒法涵蓋JVM中所做的全部性能優化,相反,我但願能夠激發你的興趣,使你主動挖掘並學習編譯器技術中所包含了數百種高級優化技術(參見相關資源)。


死代碼剔除


死代碼剔除指的是,將用於沒法被調用的代碼,即「死代碼」,從源代碼中剔除。若是編譯器在運行時發現某些指令是沒必要要的,它會簡單的將其從可執行指令集中剔除。例如,在Listing 1中,變量被賦予了肯定值,卻從未被使用,所以能夠在執行時將其徹底忽略掉。在字節碼這一層級,也就不會有將數值載入到寄存器的操做。沒有載入操做意味着能夠更少的CPU時間,更好的運行性能,尤爲是當這段代碼是「熱點」代碼的時候。


Listing 1中展現了示例代碼,其中被賦予了固定值的代碼從未被使用,屬於無用沒必要要的操做。


Listing 1. Dead code


1

2

3

4

5

6

7

8

9

10

int timeToScaleMyApp(boolean endlessOfResources) {

  int reArchitect = 24;

  int patchByClustering = 15;

  int useZing = 2;

 

  if(endlessOfResources)

      return reArchitect + useZing;

  else

      return useZing;

}

在字節碼這一層級,若是變量被載入但從未使用,編譯器會檢測到並剔除這個死代碼,如Listing 2所示。剔除死代碼能夠節省CPU時間,從而提高應用程序的運行速度。


Listing 2. The same code following optimization


1

2

3

4

5

6

7

8

9

10

int timeToScaleMyApp(boolean endlessOfResources) {

  int reArchitect = 24;

  //unnecessary operation removed here...

  int useZing = 2;

 

  if(endlessOfResources)

      return reArchitect + useZing;

  else

      return useZing;

}

冗餘剔除是一種相似的優化手段,經過剔除掉重複的指令來提高應用程序性能。


內聯


許多優化手段都試圖消除機器級跳轉指令(例如,x86架構的JMP指令)。跳轉指令會修改指令指針寄存器,所以而改變了執行流程。相比於其餘彙編指令,跳轉指令是一個代價高昂的指令,這也是爲何大多數優化手段會試圖減小甚至是消除跳轉指令。內聯是一種家喻戶曉並且好評如潮的優化手段,這是由於跳轉指令代價高昂,而內聯技術能夠將常常調用的、具備不容入口地址的小方法整合到調用方法中。Listing 3到Listing 5中的Java代碼展現了使用內聯的用法。


Listing 3. Caller method


1

2

3

int whenToEvaluateZing(int y) {

  return daysLeft(y) + daysLeft(0) + daysLeft(y+1);

}

Listing 4. Called method


1

2

3

4

5

6

int daysLeft(int x){

  if (x == 0)

     return 0;

  else

     return x - 1;

}

Listing 5. Inlined method


1

2

3

4

5

6

7

8

9

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; 

}

在Listing 3到Listing 5的代碼中,展現了將調用3次小方法進行內聯的示例,這裏咱們認爲使用內聯比跳轉有更多的優點。


若是被內聯的方法自己就不多被調用的話,那麼使用內聯也沒什麼意義,可是對頻繁調用的「熱點」方法進行內聯在性能上會有很大的提高。此外,通過內聯處理後,就能夠對內聯後的代碼進行進一步的優化,正如Listing 6中所展現的那樣。


Listing 6. After inlining, more optimizations can be applied


1

2

3

4

5

int whenToEvaluateZing(int y){

  if(y == 0) return y;

  else if (y == -1) return y - 1;

  else return y + y - 1;

}

循環優化


當涉及到須要減小執行循環時的性能損耗時,循環優化起着舉足輕重的做用。執行循環時的性能損耗包括代價高昂的跳轉操做,大量的條件檢查,和未經優化的指令流水線(即引發CPU空操做或額外週期的指令序列)等。循環優化能夠分爲不少種,在各類優化手段中佔有重要比重。其中值得注意的包括如下幾種:


合併循環:當兩個相鄰循環的迭代次數相同時,編譯器會嘗試將兩個循環體進行合併。當兩個循環體中沒有相互引用的狀況,即各自獨立時,能夠同時執行(並行執行)。

反轉循環:基本上將就是用do-while循環體換掉常規的while循環,這個do-while循環嵌套在if語句塊中。這個替換操做能夠節省兩次跳轉操做,可是,會增長一個條件檢查的操做,所以增長的代碼量。這種優化方式完美的展現了以少許增長代碼量爲代價換取較大性能的提高 —— 編譯器須要在運行時須要權衡這種得與失,並制定編譯策略。

分塊循環:從新組織循環體,以便迭代數據塊時,便於緩存的應用。

展開循環:減小判斷循環條件和跳轉的次數。你能夠將之理解爲將一些迭代的循環體「內聯」到一塊兒,而無需跨越循環條件。展開循環是有風險的,它有可能會下降應用程序的運行性能,由於它會影響流水線的運行,致使產生了冗餘指令。再強調一遍,展開循環是編譯器在運行時根據各類信息來決定是否使用的優化手段,若是有足夠的收益的話,那麼即便有些性能損耗也是值得的。

至此,已經簡要介紹了編譯器對字節碼層級(以及更底層)進行優化,以提高應用程序在目標平臺的執行性能的幾種方式。這裏介紹的幾種優化手段是比較經常使用的幾種,只是衆多優化技術中的幾種。在介紹優化方法時配以簡單示例和相關解釋,但願能夠洗髮你進行深度探索的興趣。更多相關內容請參見相關資源。


總結:回顧


爲知足不一樣須要而使用不一樣的編譯器。


解釋是將字節碼轉換爲本地機器指令的最簡單方式,其工做方式是基於對本地機器指令表的查找。

編譯器能夠基於性能計數器進行性能優化,可是須要消耗更多的資源(如code cache,優化線程等)。

相比於純解釋執行代碼,客戶端編譯器能夠將應用程序的執行性能提高一個數量級(約5到10倍)。

相比於客戶端編譯器,服務器端編譯器能夠將應用程序的執行性能提高30%到50%,但會消耗更多的資源。

層次編譯綜合了客戶端編譯器和服務器端編譯器的優勢,既能夠像客戶端編譯器那樣快速啓動,又能夠像服務器端編譯器那樣,在長時間收集運行時信息的基礎上,優化應用程序的性能。

目前,已經出現了不少代碼優化的手段。對編譯器來講,一個主要的任務就是分析全部的可能性,權衡使用某種優化手段的利弊,在此基礎上編譯代碼,優化應用程序的性能。


關於做者


Eva Andearsson對JVM計數、SOA、雲計算和其餘企業級中間件解決方案有着10多年的從業經驗。在2001年,她以JRockit JVM開發者的身份加盟了創業公司Appeal Virtual Solutions(即BEA公司的前身)。在垃圾回收領域的研究和算法方面,EVA得到了兩項專利。此外她仍是提出了肯定性垃圾回收(Deterministic Garbage Collection),後來造成了JRockit實時系統(JRockit Real Time)。在技術上,Eva與SUn公司和Intel公司合做密切,涉及到不少將JRockit產品線、WebLogic和Coherence整合的項目。2009年,Eva加盟了Azul System公,擔任產品經理。負責新的Zing Java平臺的開發工做。最近,她改換門庭,以高級產品經理的身份加盟Cloudera公司,負責管理Cloudera公司Hadoop分佈式系統,致力於高擴展性、分佈式數據處理框架的開發。

相關文章
相關標籤/搜索