用於直譯器 Java 程式碼的主機編譯

在以下文件中,我們區分主機編譯和客體編譯。

  • 主機編譯應用於直譯器的 Java 實作。如果直譯器在 HotSpot 上執行,則當 Truffle 直譯器作為 Java 應用程式進行 JIT 編譯(或動態編譯)時,會套用這種編譯。這種編譯會在原生映像檔產生期間提前套用。
  • 客體編譯應用於客體語言程式碼。這種編譯使用部分評估和 Futamura 投影,從 Truffle AST 和位元組碼中衍生出最佳化程式碼。

本節討論應用於 Truffle AST 和位元組碼直譯器的特定領域主機編譯。

主機內嵌 #

Truffle 直譯器的編寫旨在透過套用第一個 Futamura 投影來支援執行時期編譯。執行時期可編譯程式碼,也常稱為部分可評估程式碼,具有以下特性

  • 它自然地被設計為高效能,因為它也定義了語言在執行時期編譯後的效能。
  • 它的編寫目的是為了避免遞迴,因為遞迴程式碼無法快速進行部分評估。
  • 它避免複雜的抽象概念和第三方程式碼,因為它們通常不是為 PE 設計的。
  • 部分可評估程式碼的邊界由標註為 @TruffleBoundary 的方法、由呼叫 CompilerDirectives.transferToInterpreter() 主導的區塊或由呼叫 CompilerDirectives.inInterpreter() 保護的區塊可靠地定義。

Truffle 主機內嵌利用這些特性,並盡可能在主機編譯期間強制內嵌執行時期可編譯程式碼路徑。一般假設是,對執行時期編譯很重要的程式碼對於直譯器執行也很重要。每當偵測到 PE 邊界時,主機內嵌階段將不再進行任何內嵌決策,並將它們延遲到更適合常規 Java 程式碼的後續內嵌階段。

此階段的原始碼可以在 HostInliningPhase 中找到。

當編譯標註為 @HostCompilerDirectives.BytecodeInterpreterSwitch 的方法時,會套用 Truffle 主機內嵌。此類方法的最大節點成本可以使用 -H:TruffleHostInliningByteCodeInterpreterBudget=100000 設定原生映像檔,並在 HotSpot 上使用 -Djdk.graal.TruffleHostInliningByteCodeInterpreterBudget=100000 設定。如果標註為 @BytecodeInterpreterSwitch 的方法呼叫具有相同註釋的方法,則只要這兩種方法的成本不超過預算,該方法就會直接內嵌。換句話說,任何此類方法都將由內嵌階段處理,就好像它們是根位元組碼切換方法的一部分一樣。如果需要,這允許位元組碼直譯器切換由多個方法組成。

原生映像檔在封閉世界分析期間,會計算出所有可供執行時期編譯的方法。任何可能從 RootNode.execute(...) 存取的方法都被確定為執行時期可編譯的。對於原生映像檔,除了位元組碼直譯器切換之外,所有執行時期可編譯方法都使用 Truffle 主機內嵌進行最佳化。此類內嵌傳遞的最大節點成本可以使用 -H:TruffleHostInliningBaseBudget=5000 進行設定。在 HotSpot 上,執行時期可編譯方法的集合是未知的。因此,我們只能依賴於 HotSpot 上未標註為位元組碼直譯器切換的方法的常規 Java 方法內嵌。

每當達到編譯單元的最大預算時,內嵌將會停止。相同的預算將用於內嵌期間子樹的探索。如果無法在預算範圍內完全探索和內嵌呼叫,則不會對個別子樹做出任何決策。對於絕大多數執行時期可編譯的方法,不會達到此限制,因為它會受到自然 PE 邊界以及對 @Child 節點的執行方法的多型呼叫的阻止。如果有超過預算限制的方法,則建議透過新增更多 PE 邊界來最佳化此類節點。如果方法超過限制,則表示相同的程式碼在執行時期編譯的成本也很高。

偵錯主機內嵌 #

此階段執行的內嵌決策最好使用 -H:Log=HostInliningPhase,~CanonicalizerPhase,~GraphBuilderPhase 偵錯原生映像檔,或在 HotSpot 上使用 -Djdk.graal.Log=HostInliningPhase,~CanonicalizerPhase,~GraphBuilderPhase 偵錯。您可以使用 -Djdk.graal.LogFile=FILE 將輸出重新導向至檔案 (兩者皆適用)。

請考慮以下範例,該範例顯示了 Truffle 直譯器中先前描述的部分可評估程式碼的常見模式

class BytecodeNode extends Node {

    @CompilationFinal(dimensions = 1) final byte[] ops;
    @Children final BaseNode[] polymorphic = new BaseNode[]{new SubNode1(), new SubNode2()};
    @Child SubNode1 monomorphic = new SubNode1();

    BytecodeNode(byte[] ops) {
        this.ops = ops;
    }

    @BytecodeInterpreterSwitch
    @ExplodeLoop(kind = LoopExplosionKind.MERGE_EXPLODE)
    public void execute() {
        int bci = 0;
        while (bci < ops.length) {
            switch (ops[bci++]) {
                case 0:
                    // regular operation
                    add(21, 21);
                    break;
                case 1:
                    // complex operation in @TruffleBoundary annotated method
                    truffleBoundary();
                    break;
                case 2:
                    // complex operation protected behind inIntepreter
                    if (CompilerDirectives.inInterpreter()) {
                        protectedByInIntepreter();
                    }
                    break;
                case 3:
                    // complex operation dominated by transferToInterpreter
                    CompilerDirectives.transferToInterpreterAndInvalidate();
                    dominatedByTransferToInterpreter();
                    break;
                case 4:
                    // first level of recursion is inlined
                    recursive(5);
                    break;
                case 5:
                    // can be inlined is still monomorphic (with profile)
                    monomorphic.execute();
                    break;
                case 6:
                    for (int y = 0; y < polymorphic.length; y++) {
                        // can no longer be inlined (no longer monomorphic)
                        polymorphic[y].execute();
                    }
                    break;
                default:
                    // propagates transferToInterpeter from within the call
                    throw CompilerDirectives.shouldNotReachHere();
            }
        }
    }

    private static int add(int a, int b) {
        return a + b;
    }

    private void protectedByInIntepreter() {
    }

    private void dominatedByTransferToInterpreter() {
    }

    private void recursive(int i) {
        if (i == 0) {
            return;
        }
        recursive(i - 1);
    }

    @TruffleBoundary
    private void truffleBoundary() {
    }

    abstract static class BaseNode extends Node {
        abstract int execute();
    }

    static class SubNode1 extends BaseNode {
        @Override
        int execute() {
            return 42;
        }
    }

    static class SubNode2 extends BaseNode {
        @Override
        int execute() {
            return 42;
        }
    }
}

我們可以透過在 graal/compiler 中執行以下命令列,將此作為 Graal 儲存庫中的單元測試執行(請參閱類別 HostInliningBytecodeInterpreterExampleTest

mx unittest  -Djdk.graal.Log=HostInliningPhase,~CanonicalizerPhase,~GraphBuilderPhase -Djdk.graal.Dump=:3  HostInliningBytecodeInterpreterExampleTest

這會列印

[thread:1] scope: main
  [thread:1] scope: main.Testing
  Context: HotSpotMethod<HostInliningBytecodeInterpreterExampleTest$BytecodeNode.execute()>
  Context: StructuredGraph:1{HotSpotMethod<HostInliningBytecodeInterpreterExampleTest$BytecodeNode.execute()>}
      [thread:1] scope: main.Testing.EnterpriseHighTier.HostInliningPhase
      Truffle host inlining completed after 2 rounds. Graph cost changed from 136 to 137 after inlining:
      Root[jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.execute]
          INLINE jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.add(int, int)                      [inlined    2, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes    0, subTreeCost    8, incomplete false,  reason null]
          CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.truffleBoundary()                  [inlined   -1, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes    1, subTreeCost    0, incomplete false,  reason truffle boundary]
          INLINE com.oracle.truffle.api.CompilerDirectives.inInterpreter()                                                                    [inlined    0, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes    0, subTreeCost    6, incomplete false,  reason null]
          CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.protectedByInIntepreter()          [inlined   -1, monomorphic false, deopt false, inInterpreter  true, propDeopt false, subTreeInvokes    1, subTreeCost    0, incomplete false,  reason protected by inInterpreter()]
          INLINE com.oracle.truffle.api.CompilerDirectives.transferToInterpreterAndInvalidate()                                               [inlined    3, monomorphic false, deopt  true, inInterpreter false, propDeopt false, subTreeInvokes    0, subTreeCost   32, incomplete false,  reason null]
            INLINE com.oracle.truffle.api.CompilerDirectives.inInterpreter()                                                                  [inlined    3, monomorphic false, deopt  true, inInterpreter false, propDeopt false, subTreeInvokes    0, subTreeCost    6, incomplete false,  reason null]
            CUTOFF com.oracle.truffle.runtime.hotspot.AbstractHotSpotTruffleRuntime.traceTransferToInterpreter()                              [inlined   -1, monomorphic false, deopt  true, inInterpreter  true, propDeopt false, subTreeInvokes    0, subTreeCost    0, incomplete false,  reason dominated by transferToInterpreter()]
          CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.dominatedByTransferToInterpreter() [inlined   -1, monomorphic false, deopt  true, inInterpreter false, propDeopt false, subTreeInvokes    0, subTreeCost    0, incomplete false,  reason dominated by transferToInterpreter()]
          INLINE jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.recursive(int)                     [inlined    4, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes    1, subTreeCost   20, incomplete false,  reason null]
            CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.recursive(int)                   [inlined   -1, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes    1, subTreeCost    0, incomplete false,  reason recursive]
          INLINE jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode$SubNode1.execute()                 [inlined    1, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes    0, subTreeCost    6, incomplete false,  reason null]
          CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode$BaseNode.execute()                 [inlined   -1, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes    1, subTreeCost    0, incomplete false,  reason not direct call: no type profile]
          CUTOFF com.oracle.truffle.api.CompilerDirectives.shouldNotReachHere()                                                               [inlined   -1, monomorphic false, deopt false, inInterpreter false, propDeopt  true, subTreeInvokes    0, subTreeCost   98, incomplete false,  reason propagates transferToInterpreter]

請注意,我們也使用了 -Djdk.graal.Dump=:3 選項,該選項會將圖表傳送至任何正在執行的 IdealGraphVisualizer 實例以進行進一步檢查。在原生映像檔上,使用 -H:Dump=:2 -H:MethodFilter=... 來傾印給定方法的主機編譯圖。

要偵錯不完整探索的 CUTOFF 決策 (具有 incomplete true 的項目),請使用 -Djdk.graal.TruffleHostInliningPrintExplored=true 選項,以查看記錄中的所有不完整子樹。

調整主機內嵌 #

在了解如何偵錯和追蹤主機內嵌決策後,現在該看看調整它的一些方法了。第一步是識別對於良好的直譯器效能至關重要的編譯單元。為此,可以透過將 engine.Compilation 旗標設定為 false,在僅限直譯器模式下執行 Truffle 直譯器。之後,可以使用 Java 分析器來識別執行中的熱點。如需有關分析的更多詳細資訊,請參閱 Profiling.md 如果您正在尋找有關如何以及何時最佳化 Truffle 直譯器的建議,請參閱 Optimizing.md

在識別熱點方法後,例如 Truffle 位元組碼直譯器中的位元組碼分派迴圈,我們可以依照上一節所述,使用主機內嵌記錄進一步調查。有趣的項目會以 CUTOFF 為前綴,並具有 reason 來解釋個別截斷的原因。

CUTOFF 項目的常見原因包括

  • dominated by transferToInterpreter()protected by inInterpreter():這表示呼叫是在慢速路徑中執行的。主機內嵌將不會對此類呼叫做出決定,而只會將其標記為 CUTOFF。
  • target method not inlinable 這發生於無法內嵌的主機 VM 方法。我們通常對此無能為力。
  • Out of budget 我們用完了內嵌此方法的預算。如果方法的成本變得太高,就會發生這種情況。

此外,為了避免程式碼大小暴增,主機內嵌具有內建的啟發式方法,用於偵測被認為太複雜而無法內嵌的呼叫子樹。例如,追蹤可能會列印以下內容

CUTOFF com.oracle.truffle.espresso.nodes.BytecodeNode.putPoolConstant(VirtualFrame, int, char, int)   [inlined   -1, explored    0, monomorphic false, deopt false, inInterpreter false, propDeopt false, graphSize 1132, subTreeCost 5136, invokes    1, subTreeInvokes   12, forced false, incomplete false,  reason call has too many fast-path invokes - too complex, please optimize, see truffle/docs/HostOptimization.md

這表示子樹中存在太多快速路徑呼叫 (預設為 10 個),並且也會在該數字之後停止探索。可以提供 -Djdk.graal.TruffleHostInliningPrintExplored=true 旗標來查看決策的整個子樹。以下呼叫被視為快速路徑呼叫

  • 目標方法由 @TruffleBoundary 註釋的呼叫。
  • 多型呼叫或沒有可用的單型分析回饋的呼叫。例如,對子運算式的執行方法的呼叫。
  • 遞迴的呼叫。
  • 本身太複雜的呼叫。例如,具有太多快速路徑呼叫的呼叫。

以下呼叫被視為快速路徑呼叫

  • 可以使用主機內嵌啟發式方法內嵌的呼叫。
  • 慢速路徑中的呼叫,例如由 transferToInterpreter() 主導或受 isInterpreter() 保護的任何呼叫。
  • 由於主機 VM 的限制而無法內嵌的呼叫,例如對 Throwable.fillInStackTrace() 的呼叫。
  • 不再可存取的呼叫。

不可能完全避免快速路徑呼叫,因為,例如,需要在 AST 中執行子節點。從理論上講,可以在位元組碼直譯器中避免所有快速路徑呼叫。實際上,語言會依賴 @TruffleBoundary 來執行時期以實作更複雜的位元組碼。

在以下各節中,我們將討論有關如何改進主機直譯器程式碼的技術

最佳化:使用 @HostCompilerDirectives.InliningCutoff 手動切割程式碼路徑 #

如上一節所述,啟發式方法會自動截斷其中呼叫過多的內嵌子樹。一種最佳化方法是使用 @InliningCutoff 註釋。

請考慮以下範例

abstract class AddNode extends Node {

   abstract Object execute(Object a, Object b);

   @Specialization int doInt(int a, int b) { return a + b; }
   
   @Specialization double doDouble(double a, double b) { return a + b; }
   
   @Specialization double doGeneric(Object a, Object b, @Cached LookupAndCallNode callNode) { 
       return callNode.execute("__add", a, b); 
   }
}

在這個範例中,特化方法 doIntdoDouble 非常簡單,但還有一個 doGeneric 特化方法,它會調用一個複雜的查找鏈。假設 LookupAndCallNode.execute 是一個非常複雜的方法,其中包含十多個快速路徑的子樹調用,我們無法期望 execute 方法被內聯。主機內聯目前不支援自動元件分析;雖然可以使用 @InliningCutoff 注釋手動指定。

abstract class AddNode extends Node {

   abstract Object execute(Object a, Object b);

   @Specialization int doInt(int a, int b) { return a + b; }
   
   @Specialization double doDouble(double a, double b) { return a + b; }
   
   @HostCompilerDirectives.InliningCutoff
   @Specialization double doGeneric(Object a, Object b, @Cached LookupAndCallNode callNode) { 
       return callNode.execute("__add__", a, b); 
   }
}

更改程式碼後,如果 AddNode 的 execute 方法符合主機內聯的預算,主機內聯現在可能會決定內聯該方法,但強制在 doGeneric(...) 方法調用處使用 CUTOFF。請參閱 javadoc 以了解此註釋的其他使用案例。

最佳化:在部分求值期間折疊的分支中去除重複的調用 #

以下是一個範例,其中程式碼使用部分求值進行編譯時很有效率,但不適用於主機編譯。

@Child HelperNode helperNode;

final boolean negate;
// ....

int execute(int argument) {
	if (negate) {
		return helperNode.execute(-argument);
	} else {
         return helperNode.execute(argument);
	}
}

當使用部分求值編譯此程式碼時,此程式碼很有效率,因為該條件保證會折疊為單一情況,因為 negate 欄位是編譯最終的。在主機最佳化期間,negate 欄位不是編譯最終的,編譯器可能會內聯程式碼兩次,或是決定不內聯 execute 方法。為了避免這種情況,可以將程式碼重寫如下:

@Child HelperNode helperNode;

final boolean negate;
// ....

int execute(int argument) {
    int negatedArgument;
    if (negate) {
        negatedArgument = -argument;
    } else {
        negatedArgument = argument;
    }
    return helperNode.execute(negatedArgument);
}

如果使用了許多具有相同方法主體的特化方法,則類似的程式碼模式可能會透過程式碼產生間接出現。主機編譯器通常難以自動最佳化此類模式。

最佳化:在單獨的方法中提取複雜的慢速路徑程式碼 #

請考慮以下範例

int execute(int argument) {
	if (argument == 0) {
	   CompilerDirectives.transferToInterpeterAndInvalidate();
	   throw new RuntimeException("Invalid zero argument " + argument);
	}
	return argument;
}

Java 編譯器產生的位元組碼等效於以下程式碼

int execute(int argument) {
	if (argument == 0) {
	   CompilerDirectives.transferToInterpeterAndInvalidate();
	   throw new RuntimeException(new StringBuilder("Invalid zero argument ").append(argument).build());
	}
	return argument;
}

雖然此程式碼對部分求值有效率,但此程式碼在主機內聯期間會佔用不必要的空間。因此,建議為程式碼的慢速路徑部分提取單一方法

int execute(int argument) {
	if (argument == 0) {
	   CompilerDirectives.transferToInterpeterAndInvalidate();
	   throw invalidZeroArgument(argument);
	}
	return argument;
}

RuntimeException invalidZeroArgument(int argument) {
   throw new RuntimeException("Invalid zero argument " + argument);
}

與我們聯繫