インタプリタJavaコードのホスト・コンパイル
次のドキュメントでは、ホスト・コンパイルとゲスト・コンパイルのあいまいさを解消します。
- ホスト・コンパイルは、インタプリタのJava実装に適用されます。インタプリタがHotSpotで実行されている場合、この種のコンパイルは、TruffleインタプリタがJavaアプリケーションとしてJITコンパイル(または動的にコンパイル)されるときに適用されます。このコンパイルは、ネイティブ・イメージの生成中に事前に適用されます。
- ゲスト・コンパイルはゲスト言語コードに適用されます。この種のコンパイルでは、部分評価および二村射影を使用して、Truffle ASTおよびバイトコードから最適化されたコードを導出します。
この項では、Truffle ASTおよびバイトコード・インタプリタに適用されるドメイン固有のホスト・コンパイルについて説明します。
ホスト・インライン化
Truffleインタプリタは、第1二村射影を適用してランタイム・コンパイルをサポートするように作成されています。実行時コンパイル可能コード(部分評価可能コードとも呼ばれる)には、次の特徴があります:
- 言語の実行時コンパイル後のパフォーマンスも定義するため、当然のことながらパフォーマンスが高くなるように設計されています。
- 再帰的コードは迅速に部分評価できないため、再帰を避けるように記述されます。
- 複雑な抽象化とサード・パーティ・コードは、通常PE用に設計されていないため、これらを回避します。
- 部分評価可能なコードの境界は、
@TruffleBoundary
で注釈が付けられたメソッド、CompilerDirectives.transferToInterpreter()
へのコールで支配されているブロック、またはCompilerDirectives.inInterpreter()
へのコールで保護されているブロックによって確実に定義されます。
Truffleホストのインライン化は、これらのプロパティを活用し、実行時コンパイル可能コード・パスに対して、ホスト・コンパイル時に可能なかぎり強制的にインライン化を行います。一般的な前提として、実行時コンパイルに重要なコードは、インタプリタの実行にも重要であるとされています。PE境界が検出されると、ホストのインライン化フェーズではインライン化の決定が行われず、通常のJavaコードに適した、後のインライン化フェーズまで遅延されます。
このフェーズのソース・コードは、「HostInliningPhase」にあります。
Truffleホスト・インライン化は、@HostCompilerDirectives.BytecodeInterpreterSwitch
で注釈付けされたメソッドをコンパイルするときに適用されます。このようなメソッドの最大ノード・コストは、ネイティブ・イメージの場合は-H:TruffleHostInliningByteCodeInterpreterBudget=100000
、HotSpotの場合は-Dgraal.TruffleHostInliningByteCodeInterpreterBudget=100000
を使用して構成できます。@BytecodeInterpreterSwitch
で注釈が付いたメソッドが同じ注釈を持つメソッドを呼び出す場合、両方のメソッドのコストが予算を超えないかぎり、メソッドは直接インライン化されます。つまり、このようなメソッドは、ルート・バイトコード・スイッチ・メソッドの一部であるかのように、インライン化フェーズによって処理されます。これにより、必要に応じてバイトコード・インタプリタ・スイッチを複数のメソッドで構成できます。
ネイティブ・イメージは、クローズド・ワールド分析中に、実行時コンパイルのために到達可能なすべてのメソッドを計算します。RootNode.execute(...)
から到達可能な可能性のあるメソッドは、実行時コンパイル可能と判断されます。ネイティブ・イメージの場合、バイトコード・インタプリタ・スイッチに加えて、すべての実行時コンパイル可能メソッドは、Truffleホスト・インライン化を使用して最適化されます。このようなインライン・パスの最大ノード・コストは、-H:TruffleHostInliningBaseBudget=5000
で構成できます。HotSpotでは、実行時コンパイル可能メソッドのセットが不明です。したがって、HotSpotのバイトコード・インタプリタ・スイッチとして注釈が付けられていないメソッドについては、通常のJavaメソッド・インライン化のみに依存できます。
コンパイル単位の最大予算に達すると、インライン化は停止されます。同じ予算がインライン化中にサブツリーの探索に使用されます。予算内でコールを完全に探索およびインライン化できない場合、個々のサブツリーについて決定されません。実行時コンパイル可能なメソッドの大部分では、この制限に達しません。これは、@Child
ノードのメソッドを実行するためのポリモーフィック・コールだけでなく、自然PE境界によっても回避されるためです。予算制限を超えるメソッドがある場合、PE境界を追加して、このようなノードを最適化することをお薦めします。メソッドが制限を超える場合、同じコードは実行時コンパイルのコストも高くなる可能性があります。
ホスト・インライン化のデバッグ
このフェーズで実行されるインライン化の決定は、ネイティブ・イメージの場合は-H:Log=TruffleHostInliningPhase,~CanonicalizerPhase,~GraphBuilderPhase
、HotSpotの場合は-Dgraal.Log=TruffleHostInliningPhase,~CanonicalizerPhase,~GraphBuilderPhase
を使用してデバッグするのが最適です。
前述した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 -Dgraal.Log=TruffleHostInliningPhase,~CanonicalizerPhase,~GraphBuilderPhase -Dgraal.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.TruffleHostInliningPhase
Truffle host inlining completed after 2 rounds. Graph cost changed from 136 to 137 after inlining:
Root[org.graalvm.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.execute]
INLINE org.graalvm.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 org.graalvm.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 org.graalvm.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 org.graalvm.compiler.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 org.graalvm.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 org.graalvm.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 org.graalvm.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 org.graalvm.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 org.graalvm.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]
さらに検査するために、実行中のIdealGraphVisualizer
インスタンスにグラフを送信する-Dgraal.Dump=:3
オプションも使用したことに注意してください。不完全な探索(incomplete true
のエントリ)のCUTOFF決定をデバッグするには、-Dgraal.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)。また、この数を超えると探索が停止されます。-Dgraal.TruffleHostInliningPrintExplored=true
フラグを指定すると、決定のサブツリー全体を表示できます。次のコールは、ファストパス呼出しとみなされます:
- ターゲット・メソッドに
@TruffleBoundary
の注釈が付けられている呼出し。 - 多相の呼出し。または単相プロファイリング・フィードバックが使用できない呼出し。たとえば、部分正規表現の実行メソッドへのコールです。
- 再帰的な呼出し。
- 複雑すぎる呼出し。たとえば、ファストパス呼出しが多すぎる呼出し。
次のコールは、ファストパス呼出しとみなされません:
- ホスト・インライン化のヒューリスティックを使用してインライン化できる呼出し。
- スローパスでの呼出し。
transferToInterpreter()
がほとんどの呼出しやisInterpreter()
によって保護されている呼出しなど。 - ホストVMの制限のためにインライン化できない呼出し。
Throwable.fillInStackTrace()
へのコールなど。 - アクセスできなくなった呼出し。
ファストパス呼出しを完全に回避することはできません。たとえば、子ノードは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を超えるファストパス・サブツリー・コールを含む非常に複雑なメソッドであると仮定すると、実行メソッドがインライン化されることは期待できません。現在、ホスト・インライン化では自動コンポーネント分析をサポートされていません。ただし、@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
の実行メソッドのインライン化を決定できるようになります(ホスト・インライン化の予算に収まる場合)。ただし、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回インライン化します。または実行メソッドをインライン化しないことを決定します。これを回避するために、コードを次のように書き換えることができます:
@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;
}
このコードは、部分的評価にとっては効率的ですが、ホスト・インライン化の際に不要なスペースを占めます。したがって、コードのスローパス部分の1つのメソッドを抽出することをお薦めします:
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);
}