- 適用於 JDK 23 的 GraalVM (最新版本)
- 適用於 JDK 24 的 GraalVM (搶先體驗版)
- 適用於 JDK 21 的 GraalVM
- 適用於 JDK 17 的 GraalVM
- 封存
- 開發組建
- Truffle 語言實作框架
- Truffle 分支檢測
- 動態物件模型
- 靜態物件模型
- 直譯器程式碼的主機最佳化
- Truffle 的函式內嵌方法
- 分析 Truffle 直譯器
- Truffle Interop 2.0
- 語言實作
- 使用 Truffle 實作新語言
- Truffle 語言和工具遷移至 Java 模組
- Truffle 原生函數介面
- 最佳化 Truffle 直譯器
- 選項
- 堆疊上替換
- Truffle 字串指南
- 特化直方圖
- 測試 DSL 特化
- 基於 Polyglot API 的 TCK
- Truffle 編譯佇列方法
- Truffle 程式庫指南
- Truffle AOT 概觀
- Truffle AOT 編譯
- 輔助引擎快取
- Truffle 語言安全點教學
- 單態化
- 分割演算法
- 單態化使用案例
- 向執行時期報告多型特化
用於直譯器 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);
}
}
在這個範例中,特化方法 doInt
和 doDouble
非常簡單,但還有一個 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);
}