スタック上置換(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コンパイルのデバッグの詳細は、「デバッグ」を参照してください。