晚期(執行期)優化——編譯優化技術
文章目錄
為什麼編譯方式執行原生代碼比解釋方式執行位元組碼快?
- 虛擬機器解釋執行位元組碼需要額外消耗時間(難道是從位元組碼轉換成機器碼的時間,我有想到了池這種技術)
- 虛擬機器團隊對程式碼所有的優化措施都集中在即時編譯器之中。
一、優化技術概覽
型別 | 優化技術 |
---|---|
編譯器策略 | |
基於效能監控的優化技術 |
基於證據的優化技術
下面舉一個例子說明一下:
下面是他們的優化步奏,需要注意的是,優化都是發生在程式碼的某種中間表示或機器碼之上。
- 第一步:final方法內聯
public void foo() { y = b.value; ...do stuff..
- 第二步:消除冗餘訪問
public void foo() { y = b.value; ...do stuff... z = y;//b.value與y值一樣 sum = y + z; }
- 第三步:複寫傳播
public void foo() { y = b.value; ...do stuff... y = y;//z值和y一樣,沒有必要建立多餘的變數 sum = y + y; }
- 第四部:無用程式碼消除
//y=y是沒有意義的 public void foo() { y = b.value; ...do stuff.
二、公共子表示式消除
什麼叫做公共子表示式消除?
如果對於表示式E,如果之前計算過了,並且該表示式中的變數值沒有被修改,那麼下次遇到這個表示式時,就沒有必要再次進行計算了。如:
//其中c*b就相當於E,當下次遇到E時(也就是b*c),就不用再次計算E的值了
int d=(c*b)*12+a+(a+b*c);
三、陣列邊界檢查消除
Java中,對與陣列的訪問,每一次都要判斷陣列是否越界。雖然這帶來了安全,但是每一次都存在隱含的條件判定語句,這成為了一種效能負擔。
於是就考慮在哪些情況下消除陣列邊界檢查。如,array[3],如果索引是個常量,在編譯期就可以確定array.length(),進而判斷是否越界,於是在執行時就不用判斷了。還有迴圈體進行陣列訪問,使用迴圈變數作為索引,在編譯的時候判斷迴圈變數是否在[0,array.length)之間,如果在就可以去掉資料邊界檢查。
從更高角度看,比如NullPointerException、ArthmeticException(除零錯誤),C++中它不管,還是要執行,執行炸了就退出。然而Java中要先判斷執行條件是否成立,不成立就不執行。相對於在編譯器完成檢查之外,還有另一種在執行時檢查的方式——隱式異常處理。
下面偽程式碼表示訪問foo.value的過程。
if(foo!=null)
{
return foo.value;
}else{
throw new NullPointException();
}
經過隱式異常優化之後:
try{
return foo.value;
}catch(segment_fault){
uncommon_trap();
}
這樣的話就可以省去一次判斷的開銷了。然而,當foo真為空時,丟擲異常所需時間比一次判斷多得去了。當foo很少為空時,這種方法是值的。對於這種情況,我們的虛擬機器足夠聰明去選擇一個最優的優化方案!
之前面試,面試過問我為什麼選擇java,不選擇C++。當時我是這樣回答的:java簡單啊!
現在想來,好膚淺啊。選擇java還是C++要看你的業務,公司的需求。如果你為了追求安全性,可以選擇Java,如果你最求效能,可以選擇C++(前提是你C++會寫,不然非但效能得不到,還會影響安全)。
四、方法內聯
4.1 為什麼要進行方法內聯
- 它可以消除方法呼叫的成本
- 為其他優化手段建立良好的基礎
4.2 例子
下面這個例子,如果不做內聯,無法進行無用程式碼優化。
public static void foo(Object obj)
{
if(obj!=null)
{
System.out.println("do something");
}
}
public static void testInline(String[] args){
Object obj=null;
foo(obj);
}
分成兩個方法看,兩個方法都是有意義的。然而,內聯之後你就會發現那都是無用程式碼。所以內聯為其他優化手段建立了良好的基礎。
4.3 內聯和虛方法之間的矛盾
由於虛方法的確定是在執行是才知道的,而內聯是在方法執行之前進行的,我們又想對虛方法進行內聯。因此內聯和虛方法之間就這樣產生矛盾了。
4.4 解決辦法
處理這個問題,需要用到型別繼承關係分析(CHA)和內聯快取,還有逃生門。
五、逃逸分析
5.1 什麼叫做方法逃逸、執行緒逃逸
方法逃逸:
方法內部定義的變數,被外部方法引用。如作為呼叫引數傳遞到外部方法中
執行緒逃逸:
被外部執行緒訪問的就叫做執行緒逃逸。如賦值給類變數或可以在其他執行緒中訪問的例項變數。
5.2 沒有方法逃逸、執行緒逃逸之後可以幹什麼
- 棧上分配。因為物件不被其他方法訪問,當方法結束後,就可以隨棧幀的消失而自動釋放記憶體
- 同步消除。都不會發生執行緒逃逸了,當然可以將多餘的同步控制消除掉。
- 標量替換。java虛擬機器中的原始資料型別稱為標量。將方法訪問到的對像中的成員變數恢復成原始型別來訪問稱為標量替換。這樣就可以不用建立物件了。
需要注意的是,這項技術還不成熟。預設是沒有開啟的。