java 优化 寄存器_JVM性能优化系列-(6) 晚期编译优化
6. 晚期編譯優化
晚期編譯優化主要是在運行時做的一些優化手段。
6.1 JIT編譯器
在部分的商用虛擬機中,java程序最初是通過解釋器(Interpreter) 進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定為“熱點代碼”(Hot Spot Code)。為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,并進行各種層次的優化,完成這個過程的編譯器稱為即時編譯器(Just In Time Compiler)
java虛擬機規范中沒有規定即時編譯器應該如何實現,也沒有規定虛擬機必需擁有即時編譯器,這部分功能完全是虛擬機具體實現相關的內容。本文中提及的編譯器、即時編譯器都是指HotSpot虛擬機內的即時編譯器。
6.2 HotSpot虛擬機內的即時編譯器
解釋器和編譯器
HotSpot虛擬機采用解釋器與編譯器并存的架構,解釋器與編譯器兩者各有優勢:
當程序需要迅速啟動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行
在程序運行后,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲得更高的執行效率
當程序運行環境中內存資源限制較大,可以使用解釋執行節約內存,反之可以使用編譯執行來提升效率
解釋器還可以作為編譯器激進優化的一個“逃生門”,讓編譯器根據概率選擇一些大多數時候都能提升運行速度的優化手段,當激進優化的假設不成立時,可以通過逆優化退回到解釋狀態繼續執行。
HotSpot虛擬機中內置了兩個即時編譯器,分別稱為Client Compiler和Server Compiler,或者簡稱為C1編譯器和C2編譯器,虛擬機默認采用解釋器與其中一個編譯器直接配合的方式工作
由于即時編譯器編譯本地代碼需要占用程序運行時間,要編譯出優化程度更高的代碼,所花費的時間可能更長;而且想要編譯出優化程度更高的代碼,解釋器可能還要替編譯器收集性能監控信息,這對解釋執行的速度也有影響。HotSpot虛擬機采用分層編譯(Tiered Compilation)的策略,其中包括:
第0層:程序解釋執行,解釋器不開啟性能監控功能(Profiling),可觸發第1層編譯
第1層:也稱為C1編譯,將字節碼編譯為本地代碼,進行簡單、可靠的優化,如有必要將加入性能監控的邏輯
第2層:也稱為C2編譯,也是將字節碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化
編譯對象與觸發條件
在運行過程中會被即時編譯器編譯的“熱點代碼”有兩類:
被多次調用的方法
被多次執行的循環體
在這兩種情況下,都是以整個方法作為編譯對象,這種編譯方式被稱為棧上替換。
判斷一段代碼是不是熱點代碼,是不是需要觸發即時編譯,這樣的行為稱為熱點探測(Hot Spot Detection),目前主要的熱點探測判定方式有兩種:
基于采樣的熱點探測(Sample Based Hot Spot Detection):采用這種方法的虛擬機會周期性地檢查各個線程地棧頂,如果發現某個方法經常出現在棧頂,那這個方法就是“熱點方法”
基于計數器的熱點探測(Counter Based Hot Spot Detection):采用這個種方法的虛擬機會為每個方法建立計數器,統計方法的執行次數,如果執行次數超過一定的閾值就認為它是“熱點方法”
在HotSpot虛擬機中使用的是第二種,因此它為每個方法準備了兩類計數器:方法調用計數器(Invocation Counter)和回邊計數器(Back Edge Counter)。在確定虛擬機運行參數的前提下,這兩個計數器都由一個確定的閾值,當計數器超過閾值溢出了,就會觸發JIT編譯。
方法調用計數器:用于統計方法被調用的次數,它的默認閾值在Client模式下是1500次,在Server模式在是10000次,可通過-XX: CompileThreshold來設定。
如果不做任何設置,方法調用計數器統計的不是方法被調用的絕對次數,而是一個相對執行頻率,即一段時間之內方法被調用的次數。當超過一定的時間限度,如果方法的調用次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的調用計數器就會被減少一半,這個過程稱為方法調用計數器的熱度衰減(Counter Decay)
回邊計數器:用于統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向后跳轉的指令稱為“回邊”(Back Edge)。
與方法計數器不同,回邊計數器沒有計數熱度衰減的過程,因此這個計數器統計的就是該方法循環執行的絕對次數。
6.3 編譯過程
Server Compiler和Client Compiler兩個編譯器的編譯過程是不一樣的。
Client Compiler是一個簡單快速的三段式編譯器,主要的關注點在于局部性的優化,而放棄了許多耗時較長的全局優化手段。
第一個階段:使用一個平臺獨立的前端將字節碼構造成一種高級中間代碼表示(High-Level Intermediate Representaion, HIR)。HIR使用靜態單分配(Static Single Assignment, SSA)的形式來代表代碼值,這可以使得一些在HIR的構造過程之中和之后進行的優化動作更容易實現。在此之前編譯器會在字節碼上完成一部分基礎優化,如方法內聯、常量傳播等
第二個階段:使用一個平臺相關的前端從HIR中產生低級中間代碼表示(Low-Level Intermediate Representaion, LIR),而在此之前會在HIR上完成另外一些優化,如空值檢查清除、范圍檢查清除等
最后階段:使用平臺相關的后端使用線性掃描算法(Linear Scan Register Allocation)在LIR上分離寄存器,并在LIR上做窺孔(Peephole)優化,然后產生機器代碼
Server Compiler是專門面向服務端的典型應用并為服務端的性能配置特別調整過的編譯器,它會執行所有經典的優化動作。Server Compiler的寄存器分配器是一個全局圖著色分配器,它可以充分利用某些處理器架構上的大寄存器集合。以即時編譯的標準來看,Server Compiler編譯速度比較緩慢,但依然遠遠超過傳統的靜態優化編譯器,而且相對于Client Compiler編譯輸出的代碼質量有所提高,可以減少本地代碼的執行時間,從而抵消了額外的編譯時間開銷.
6.4 編譯優化技術
在即時編譯器中采用的優化技術有很多,本節主要針對以下四種優化技術:
語言無關的經典優化技術之一:公共子表達式消除
語言相關的經典優化技術之一:數組范圍檢查消除
最重要的優化技術之一:方法內聯
最前沿的優化技術之一:逃逸分析
公共子表達式消除
公共子表達式消除是一個普遍應用與各種編譯器的經典優化技術,它的含義是:
如果一個表達式E已經計算過了,并且從先前的計算到現在E中的所有變量的值都沒有發生變化,那么E的這次出現就成為了公共子表達式
對于這種表達式,沒有必要花時間再對它進行計算,只需要直接用前面計算過的表達式結果替代E就可以了
如果這種優化僅限于程序的基本塊內,便稱為局部公共子表達式消除(Local Common Subexpression Elimination),如果這種優化的范圍涵蓋了多個基本塊,那就稱為全局公共子表達式消除(Global Common Subexpression Elimination)。
數組邊界檢查消除
組邊界檢查消除(Array Bounds Checking Elimination)是即時編譯器中的一項語言相關的經典優化技術。由于java語言中訪問數組元素時,系統將會自動進行上下界的范圍檢查,這必定會造成性能負擔。為了安全,數組邊界檢查是必須做的,但數組邊界檢查是否必須一次不漏的執行則是可以“商量”的事情。例如編譯器通過數據流分析判定數組下標的取值永遠在[0,數組.length)之內,就可以把數組的上下界檢查消除
從更高的角度看,大量安全檢查使編寫java程序更簡單,但也造成了更多的隱式開銷,對于這些隱式開銷,除了盡可能把運行期檢查提到編譯期完成的思路之外,還可以使用隱式異常處理:
if(x != null){
return x.value;
}else{
throw new NullPointException();
}
隱式異常優化后:
try{
return x.value;
}catch(segment_fault){
uncommon_trap();
}
虛擬機會注冊一個Segment Fault信號的異常處理器(uncommon_trap()),這樣x不為空時,不會額外消耗一次對foo判空的開銷。代價是當x為空時,必須轉入異常處理器中恢復并拋出NullPointException,速度遠比一次判空檢查慢。
方法內聯
方法內聯是編譯器最重要的優化手段之一,除了消除方法調用成本之外,更重要的意義是為其他優化手段建立良好的基礎。方法內聯的優化行為只不過是把目標方法的代碼“復制”到發起調用的方法之中,避免發生真實的方法調用而已。但實際上java虛擬機中的內聯過程遠遠沒有那么簡單,因為java中的方法大多數是虛方法,虛方法在編譯期做內聯的時候根本無法確定應該使用哪個方法版本
對此java虛擬機設計團隊想了很多辦法,首先是引入了一種名為“類型繼承關系分析”(Class Hierarchy Analysis, CHA)的技術,這是一種基于整個應用程序的類型分析技術,它用于確定在目前已加載的類中,某個接口是否有多余一種的實現,某個類是否存在子類、子類是否為抽象類等信息。
編譯器在進行內聯:
非虛方法:直接進行內聯,這時候的內聯是有穩定前提保障的
虛方法:向CHA查詢此方法只在當前程序下是否有多個目標版本可供選擇
只有一個:可以進行內聯,不過這種內聯屬于激進優化,需要預留一個“逃生門”,稱為守護內聯(Guarded Inlining)。如果程序的后續執行過程中,虛擬機一直沒有加載到會令這個方法的接收者的繼承關系發生變化的類,那這個內聯游湖的代碼就可以一直使用下去。否則,就需要拋棄已經編譯的代碼,退回到解釋狀態執行,或者重新進行編譯
有多個版本:編譯器還將進行最后一次努力,使用內聯緩存(Inline Cache)來完成方法內聯。工作原理大致是:在未發生方法調用之前,內聯緩存狀態為空,當第一次調用發生后,緩存記錄下方法接收者的版本信息,并且每次進行方法調用時都比較接收者版本,如果以后進來的每次調用的方法接收者版本都是一樣的,那這個內聯還可以一直用下去。如果發生了方法接收者不一致的情況,就說明程序真正使用了虛方法的多態特性,這時才會取消內聯,查找虛方法表進行方法分派
逃逸分析
逃逸分析(Escape Analysis)是目前java虛擬機中比較前沿的優化技術,它與類型繼承關系分析一樣,并不是直接優化代碼的手段,而是為其他優化手段提供依據的分析技術。其基本行為是分析對象動態作用域:當一個對象在方法中被定義后,它可能被外部方法所引用,如作為調用參數傳遞到其他方法中,稱為方法逃逸;被外部線程訪問到,稱為線程逃逸。
如果能證明一個對象不會逃逸到方法或線程之外,則可能為這個變量進行一些高效的優化:
棧上分配(Stack Allocation):將對象在棧上分配內存,這樣就可以使對象所占內存空間隨棧幀出棧而銷毀,減小垃圾收集系統的壓力
同步消除(Synchronization Elimination):對象無法被其他線程訪問,這個變量的讀寫肯定不會有競爭,對這個變量實施的同步措施也就可以消除掉
標量替換(Scalar Replacement):標量(Scalar)是指一個數據已經無法再分解成更小的數據來表示。如果逃逸分析證明一個對象不會被外部訪問,并且這個對象可以被拆散的話,那程序真正執行的時候可能不創建這個對象,而直接創建它的成員變量來代替。將對象拆分后,除了可以讓對象的成員變量在棧上分配和讀寫之外,還可以為后續進一步的優化手段創建條件。
6.5 java與C/C++的編譯器對比
java與C/C++的編譯器對比實際上代表了最經典的即時編譯器與靜態編譯器的對比。java虛擬機的即時編譯器與C/C++的靜態優化編譯器相比,可能會由于下列原因而導致輸出的本地代碼有一些劣勢:
即時編譯器運行時占用的是用戶程序的運行時間,因此即時編譯器不敢隨便引入大規模的優化技術,而編譯的時間成本在靜態優化編譯器中并不是主要的關注點
java語言是動態的類型安全語言,這就意味著虛擬機必須頻繁地進行安全檢查
java語言中虛方法的使用頻率遠遠大于C/C++語言,導致即時編譯器在進行一些優化時的難度要遠大于C/C++的靜態優化編譯器
java語言時可以動態擴展的語言,運行時加載新的類可能改變程序類型的繼承關系,導致許多全局的優化措施都只能以激進優化的方式來完成
java虛擬機中對象的內存分配都是在堆上進行的,而C/C++的對象則有多種分配方式,而且C/C++中主要由用戶程序代碼來回收分配的內存,因此運行效率上比垃圾收集機制要高
上面說的java語言相對C/C++的劣勢都是為了換取開發效率上的優勢而付出的代價,而且還有許多優化是java的即時編譯器能做而C/C++的靜態優化編譯器不能做或者不好做的,如別名分析、調用頻率預測、分支頻率預測、裁剪為被選擇的分支等.
本文由『后端精進之路』原創,首發于博客 http://teckee.github.io/ , 轉載請注明出處
搜索『后端精進之路』關注公眾號,立刻獲取最新文章和價值2000元的BATJ精品面試課程。
總結
以上是生活随笔為你收集整理的java 优化 寄存器_JVM性能优化系列-(6) 晚期编译优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: QQ厘米秀在哪 厘米秀怎么玩
- 下一篇: 狙击精英3机场远离狙击窝点怎么过