Java中的方法內聯
1. 什麼是方法內聯
例如有下面的原始程式碼:
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.get();
// ...do stuff...
z = b.get();
sum = y + z;
}
我們首先要進行的就是方法內聯,主要有下面兩個目的:
- 去除方法呼叫的成本,如查詢方法版本、建立棧幀。
- 為其他優化建立良好基礎。
內聯後程式碼如下:
public void foo() {
y = b.value;
// ...do stuff...
z = b.value;
sum = y + z;
}
後續,還可以進行冗餘訪問消除、複寫傳播、無用程式碼消除等優化操作。
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);
}
testInline()
方法裡其實全都是無用的程式碼,但是如果不做方法內聯,就無法發現任何 Dead Code 的存在,因為分開看的話兩個方法裡面的操作可能都有意義。
3. Java中方法內聯的困難
在 JVM 中,只有非虛方法,也就是使用invokespecial
指令呼叫的私有方法、例項構造器、父類方法和使用invokestatic
指令呼叫的靜態方法才會在編譯器進行解析。
而其他虛方法被invokevirtual
指令呼叫,在呼叫時必須進行方法接收者的多型選擇。對於一個虛方法,編譯器靜態地去做內聯的時候很難確定應該使用哪個方法版本,這就造成了方法內聯的困難。
繼承型別關係分析 CHA
首先,JVM 引入了一種名為型別繼承關係分析 CHA 的技術,這種技術用於在已載入的類中,確定某個介面是否有多於一種的實現、某個類是否存在子類、某個子類是否覆蓋了父類的某個虛方法等資訊。
編譯器在進行內聯時會分不同情況採取不同處理:
-
如果是非虛方法,那麼就直接進行內聯。
-
如果是虛方法,那麼向 CHA 查詢是否有多個目標版本可供選擇。
-
如果只有一個版本,就直接內聯,稱為守護內聯。但由於 Java 程式動態連線,不知道什麼時候就會載入到新的型別而改變 CHA 的結論,所以要留好逃生門,假如程式後續執行中載入了導致繼承關係發生變化的新類,那麼必須拋棄已經編譯的程式碼,退回到解釋狀態進行執行,或者重新編譯。
-
如果有多個版本可供選擇,那即時編譯器使用內聯快取來縮減方法呼叫的開銷。內聯快取是一個建立在目標方法正常入口之前的快取。在未發生方法呼叫時,內聯快取為空。第一次呼叫發生後,快取記錄下方法接收者的版本資訊,並且在每次進行呼叫前都檢查版本。
- 如果每次呼叫的方法接收者版本是一樣的,那稱為單態內聯快取,通過快取來呼叫,相比不內聯只多了一次型別判斷的開銷。
- 如果出現方法接收者不一致的情況,就退化為超多型內聯快取,開銷相當於真正查詢虛方法表來進行方法分派。
- 當快取未命中的時候,大多數JVM的實現時退化成超多型內聯快取,也有一些JVM選擇重寫單態內聯快取,就是更新快取為新的版本。這樣做的好處是以後還可能會命中,壞處是可能白白浪費一個寫的開銷。
-