スタック上置換(OSR)

実行中に、Truffleは「ホット」コール・ターゲットのコンパイルをスケジュールします。ターゲットがコンパイルされると、その後ターゲットを呼び出すことでコンパイル済バージョンを実行できます。ただし、コール・ターゲットを継続して実行すると、実行をコンパイル済コードに移せないため、このコンパイルのメリットを得ることができません。つまり、長期実行中のターゲットがインタプリタで「スタック」する可能性があり、ウォームアップ・パフォーマンスを低下させます。

スタック上置換(OSR)とは、インタプリタから「取り出し」て、解釈されたコードからコンパイルされたコードに実行を移すためにTruffleで使用される手法です。Truffleでは、ASTインタプリタ(LoopNodeを含むAST)とバイトコード・インタプリタ(ディスパッチ・ループを含むノード)の両方に対してOSRがサポートされます。いずれの場合も、Truffleはヒューリスティックを使用して、長時間実行ループがいつ解釈されるかを検出し、OSRを実行して実行を高速化できます。

ASTインタプリタのOSR

標準のTruffle APIを使用する言語は、OSRをGraalで無料で利用できます。ランタイムは、LoopNode(TruffleRuntime.createLoopNode(RepeatingNode)を使用して作成された)がインタプリタで実行される回数を追跡します。ループの反復回数がしきい値を超えると、ランタイムはループが「ホット」であるとみなし、透過的にループのコンパイルと完了のポーリングを行ってから、コンパイルされたOSRターゲットをコールします。OSRターゲットは、インタプリタによって使用されるものと同じFrameを使用します。ループがOSRの実行で終了すると、解釈された実行に戻り、結果を転送します。

詳細は、LoopNode javadocを参照してください。

バイトコード・インタプリタのOSR

バイトコード・インタプリタのOSRでは、言語との連携が少し必要になります。通常、バイトコード・ディスパッチ・ノードは次のようになります:

class BytecodeDispatchNode extends Node {
  @CompilationFinal byte[] bytecode;

  ...

  @ExplodeLoop(kind = ExplodeLoop.LoopExplosionKind.MERGE_EXPLODE)
  Object execute(VirtualFrame frame) {
    int bci = 0;
    while (true) {
      int nextBCI;
      switch (bytecode[bci]) {
        case OP1:
          ...
          nextBCI = ...
          ...
        case OP2:
          ...
          nextBCI = ...
          ...
        ...
      }
      bci = nextBCI;
    }
  }
}

ASTインタプリタとは異なり、多くの場合、バイトコード・インタプリタ内のループは構造化されていません(また暗黙的です)。バイトコード言語には構造化されたループがありませんが、コード内の後方ジャンプ(バックエッジ)がループ反復に適したプロキシとして使用される傾向があります。したがって、TruffleのバイトコードOSRは、バックエッジとそのエッジの宛先(多くの場合ループ・ヘッダーに対応)を中心として設計されています。

TruffleのバイトコードOSRを使用するには、言語のディスパッチ・ノードがBytecodeOSRNodeインタフェースを実装する必要があります。このインタフェースには、少なくとも3つのメソッド実装が必要です:

メイン・ディスパッチ・ループでは、言語がバックエッジに到達したときに、指定されたBytecodeOSRNode.pollOSRBackEdge(osrNode)メソッドを呼び出してバックエッジをランタイムに通知する必要があります。ノードがOSRのコンパイルに適格であるとランタイムによって判断されると、このメソッドはtrueを返します。

pollOSRBackEdgetrueした場合のみ、言語がOSRを試行するためにBytecodeOSRNode.tryOSR(osrNode, target, interpreterState, beforeTransfer, parentFrame)をコールできます。このメソッドは、コンパイルをtargetから開始することを要求します。コンパイル済コードが使用できるようになると、後続のコールがコンパイル済コードを透過的に呼び出して、計算結果を返すことができます。interpreterStateパラメータおよびbeforeTransferパラメータについてはすぐに説明します。

OSRをサポートするように前述の例をリファクタリングすると次のようになります:

class BytecodeDispatchNode extends Node implements BytecodeOSRNode {
  @CompilationFinal byte[] bytecode;
  @CompilationFinal private Object osrMetadata;

  ...

  Object execute(VirtualFrame frame) {
    return executeFromBCI(frame, 0);
  }

  Object executeOSR(VirtualFrame osrFrame, int target, Object interpreterState) {
    return executeFromBCI(osrFrame, target);
  }

  Object getOSRMetadata() {
    return osrMetadata;
  }

  void setOSRMetadata(Object osrMetadata) {
    this.osrMetadata = osrMetadata;
  }

  @ExplodeLoop(kind = ExplodeLoop.LoopExplosionKind.MERGE_EXPLODE)
  Object executeFromBCI(VirtualFrame frame, int bci) {
    while (true) {
      int nextBCI;
      switch (bytecode[bci]) {
        case OP1:
          ...
          nextBCI = ...
          ...
        case OP2:
          ...
          nextBCI = ...
          ...
        ...
      }

      if (nextBCI < bci) { // back-edge
        if (BytecodeOSRNode.pollOSRBackEdge(this)) { // OSR can be tried
          Object result = BytecodeOSRNode.tryOSR(this, nextBCI, null, null, frame);
          if (result != null) { // OSR was performed
            return result;
          }
        }
      }
      bci = nextBCI;
    }
  }
}

バイトコードOSRのわずかな違いは、OSRの実行が、ループの終了時点を越えてコール・ターゲットの最後まで続行することです。したがって、実行がOSRから戻ってからインタプリタで実行を続行する必要はありません。単に結果をコール元に転送できます。

tryOSRに対するinterpreterStateパラメータには、実行に必要な追加のインタプリタ状態を含めることができます。この状態はexecuteOSRに渡され、実行の再開に使用できます。たとえば、インタプリタが読取り/書込みを管理するためにデータ・ポインタを使用し、それがtargetごとに一意である場合は、このポインタをinterpreterStateで渡すことができます。これはコンパイラから認識され、部分評価で使用されます。

tryOSRに対するbeforeTransferパラメータは、OSRを実行する前に呼び出されるオプションのコールバックです。tryOSRではOSRが実行される場合もされない場合もあるため、このパラメータは、OSRコードに移る前にアクションを実行する手段です。たとえば、言語がコールバックを渡して、OSRコードにジャンプする前にインストゥルメンテーション・イベントを送信することができます。

BytecodeOSRNodeインタフェースには、いくつかのフック・メソッドも含まれており、これらのデフォルトの実装はオーバーライドできます:

バイトコード・ベースのOSRは、実装が面倒な場合があります。デバッグのヒントをいくつか示します:

詳細は、BytecodeOSRNode javadocを参照してください。

コマンドライン・オプション

OSRの構成に使用できる2つの(試験段階の)オプションがあります:

デバッグ

OSRコンパイル・ターゲットは、<OSR> (または<OSR@n> (nはバイトコードOSRの場合のディスパッチ・ターゲット)でマークされます。これらのターゲットは、コンパイル・ログやIGVなどの標準デバッグ・ツールを使用して表示およびデバッグできます。たとえば、コンパイル・ログでは、バイトコードOSRエントリは次のように表示されます:

[engine] opt done     BytecodeNode@2d3ca632<OSR@42>                               |AST    2|Tier 1|Time   21(  14+8   )ms|Inlined   0Y   0N|IR   161/  344|CodeSize   1234|Addr 0x7f3851f45c10|Src n/a

Graalコンパイルのデバッグの詳細は、「デバッグ」を参照してください。