インタプリタJavaコードのホスト・コンパイル

次のドキュメントでは、ホスト・コンパイルとゲスト・コンパイルのあいまいさを解消します。

この項では、Truffle ASTおよびバイトコード・インタプリタに適用されるドメイン固有のホスト・コンパイルについて説明します。

ホスト・インライン化

Truffleインタプリタは、第1二村射影を適用してランタイム・コンパイルをサポートするように作成されています。実行時コンパイル可能コード(部分評価可能コードとも呼ばれる)には、次の特徴があります:

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エントリの一般的な理由は次のとおりです:

また、コード・サイズの急激な増大を避けるため、ホスト・インライン化には組込みのヒューリスティックがあり、インライン化には複雑すぎるとみなされるコール・サブツリーを検出します。たとえば、トレースによって次が出力されます:

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フラグを指定すると、決定のサブツリー全体を表示できます。次のコールは、ファストパス呼出しとみなされます:

次のコールは、ファストパス呼出しとみなされません:

ファストパス呼出しを完全に回避することはできません。たとえば、子ノードは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);
}