Java中的方法內聯

pedro7發表於2022-01-28

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選擇重寫單態內聯快取,就是更新快取為新的版本。這樣做的好處是以後還可能會命中,壞處是可能白白浪費一個寫的開銷。
      image-20210824231434777

相關文章