- JDK 23 用 GraalVM (最新)
- JDK 24 用 GraalVM (早期アクセス)
- JDK 21 用 GraalVM
- JDK 17 用 GraalVM
- アーカイブ
- 開発ビルド
- Truffle 言語実装フレームワーク
- Truffle ブランチインストルメンテーション
- 動的オブジェクトモデル
- 静的オブジェクトモデル
- インタープリターコードのホスト最適化
- 関数インライン化への Truffle アプローチ
- Truffle インタープリターのプロファイリング
- Truffle Interop 2.0
- 言語実装
- Truffle を使用した新しい言語の実装
- Java モジュールへの Truffle 言語およびインストゥルメントの移行
- Truffle ネイティブ関数インターフェース
- Truffle インタープリターの最適化
- オプション
- オンスタック置換
- Truffle 文字列ガイド
- 特殊化ヒストグラム
- DSL 特殊化のテスト
- ポリグロット 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 にあります。
Truffle ホストインライン化は、@HostCompilerDirectives.BytecodeInterpreterSwitch
で注釈が付けられたメソッドをコンパイルするときに適用されます。このようなメソッドの最大ノードコストは、ネイティブイメージの場合は -H:TruffleHostInliningByteCodeInterpreterBudget=100000
、HotSpot の場合は -Djdk.graal.TruffleHostInliningByteCodeInterpreterBudget=100000
を使用して構成できます。@BytecodeInterpreterSwitch
で注釈が付けられたメソッドが同じ注釈が付けられたメソッドを呼び出す場合、両方のメソッドのコストが予算を超えない限り、メソッドは直接インライン化されます。言い換えれば、このようなメソッドは、ルートバイトコードスイッチメソッドの一部であるかのように、インライン化フェーズによって処理されます。これにより、バイトコードインタープリタースイッチを必要に応じて複数のメソッドで構成できます。
ネイティブイメージは、クローズドワールド分析中に、ランタイムコンパイルのために到達可能なすべてのメソッドを計算します。RootNode.execute(...)
から到達可能な可能性のあるメソッドは、ランタイムコンパイル可能と判断されます。ネイティブイメージの場合、バイトコードインタープリタースイッチに加えて、すべてのランタイムコンパイル可能なメソッドは、Truffle ホストインライン化を使用して最適化されます。このようなインライン化パスの最大ノードコストは、-H:TruffleHostInliningBaseBudget=5000
で構成できます。HotSpot では、ランタイムコンパイル可能なメソッドのセットは不明です。したがって、HotSpot でバイトコードインタープリタースイッチとして注釈が付けられていないメソッドについては、通常の Java メソッドインライン化にのみ依存できます。
コンパイルユニットの最大予算に達すると、インライン化は停止します。インライン化中にサブツリーを探索する場合も、同じ予算が使用されます。予算内で呼び出しを完全に探索してインライン化できない場合は、個々のサブツリーに関する決定は行われません。大部分のランタイムコンパイル可能なメソッドの場合、これは自然な PE 境界と、@Child
ノードの execute メソッドへのポリモーフィック呼び出しによって防止されるため、この制限には達しません。予算制限を超えるメソッドがある場合は、より多くの 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=...
を使用して、特定のメソッドのホストコンパイルグラフをダンプします。
不完全な探索 (incomplete true
のエントリ) の CUTOFF の決定をデバッグするには、-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
で注釈が付けられている呼び出し。 - ポリモーフィックであるか、モノモーフィックプロファイリングフィードバックが利用できない呼び出し。たとえば、サブ式の execute メソッドの呼び出し。
- 再帰的な呼び出し。
- それ自体が複雑すぎる呼び出し。たとえば、高速パス呼び出しが多すぎる呼び出し。
次の呼び出しは、高速パス呼び出しとみなされません
- ホストインライン化ヒューリスティックを使用してインライン化できる呼び出し。
- 低速パスでの呼び出し。
transferToInterpreter()
によって支配される、またはisInterpreter()
によって保護される呼び出し。 Throwable.fillInStackTrace()
の呼び出しなど、ホスト VM の制限によりインライン化できない呼び出し。- 到達できなくなった呼び出し。
たとえば、子ノードは AST で実行する必要があるため、高速パス呼び出しを完全に回避することは不可能です。理論的には、バイトコードインタープリターで高速パス呼び出しをすべて回避できます。実際には、言語は @TruffleBoundary
に依存して、より複雑なバイトコードを実装します。
次のセクションでは、ホストインタープリターコードを改善する方法について説明します。
最適化: @HostCompilerDirectives.InliningCutoff を使用して手動でコードパスをカットする #
前のセクションで述べたように、ヒューリスティックは、呼び出しが多すぎるインライン化サブツリーを自動的にカットします。これを最適化する方法の 1 つは、@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
が 10 個を超える高速パスサブツリー呼び出しを含む非常に複雑なメソッドであると仮定すると、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
フィールドはコンパイル最終ではなく、コンパイラーはコードを 2 回インライン化するか、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);
}