スタック上置換(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つのメソッド実装が必要です:
executeOSR(osrFrame, target, interpreterState): このメソッドは、osrFrameを現在のプログラム状態として使用し、指定されたtarget(つまりバイトコード索引)に実行をディスパッチします。interpreterStateオブジェクトによって、実行の再開に必要な追加のインタプリタ状態を渡すことができます。getOSRMetadata()およびsetOSRMetadata(osrMetadata): これらのメソッドは、クラスに対して宣言されたフィールドにプロキシ・アクセスします。ランタイムは、これらのアクセサを使用してOSRコンパイルに関連する状態(つまりバックエッジ回数)を保持します。フィールドに@CompilationFinal注釈を付ける必要があります。
メイン・ディスパッチ・ループでは、言語がバックエッジに到達したときに、指定されたBytecodeOSRNode.pollOSRBackEdge(osrNode)メソッドを呼び出してバックエッジをランタイムに通知する必要があります。ノードがOSRのコンパイルに適格であるとランタイムによって判断されると、このメソッドはtrueを返します。
pollOSRBackEdgeがtrueした場合のみ、言語が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インタフェースには、いくつかのフック・メソッドも含まれており、これらのデフォルトの実装はオーバーライドできます:
copyIntoOSRFrame(osrFrame, parentFrame, target)およびrestoreParentFrame(osrFrame, parentFrame): 解釈されたFrameをOSRコード内で再利用することは最適ではありません。OSRコール・ターゲットをエスケープし、スカラー置換を妨げるためです(スカラー置換の背景情報は、この資料を参照してください)。可能であれば、Truffleは、copyIntoOSRFrameを使用して解釈状態(parentFrame)をOSRFrame(osrFrame)にコピーし、後からrestoreParentFrameを使用して親のFrameに状態をコピーして戻します。デフォルトでは、両方のフックがソース・フレームと宛先フレーム間で各スロットをコピーしますが、より細かく制御するためにオーバーライドできます(ライブ変数のみをコピーする場合など)。オーバーライドした場合は、スカラー置換をサポートするように、これらのメソッドを慎重に記述する必要があります。prepareOSR(target): このフックは、OSRターゲットをコンパイルする前にコールされます。これは、コンパイル前に強制的に初期化を実行するために使用できます。たとえば、インタプリタでしかフィールドを初期化できない場合、prepareOSRを使用すると確実に初期化できます。このため、OSRコードがこのフィールドにアクセスしようとして脱最適化しなくなります。
バイトコード・ベースのOSRは、実装が面倒な場合があります。デバッグのヒントをいくつか示します:
- メタデータ・フィールドが
@CompilationFinalとマークされるようにします。 - 所定の
FrameDescriptorのFrameが以前にマテリアライズされていた場合、Truffleは、コピーせずにインタプリタのFrameを使用します(コピーが使用されると、既存のマテリアライズされたFrameがOSRのFrameと同期しなくなります)。 - コンパイル・ログおよび脱最適化ログをトレースして、
prepareOSRで実行できる初期化作業を識別すると役立ちます。 - コンパイル済のOSRターゲットをIGVで調べると、フックのコピーを部分評価と適切に相互作用させるために役立ちます。
 
詳細は、BytecodeOSRNode javadocを参照してください。
コマンドライン・オプション
OSRの構成に使用できる2つの(試験段階の)オプションがあります:
engine.OSR: OSRを実行するかどうか(デフォルト:true)engine.OSRCompilationThreshold: OSRコンパイルのトリガーに必要なループ反復/バックエッジの数(デフォルト:100,352)。
デバッグ
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コンパイルのデバッグの詳細は、「デバッグ」を参照してください。