堆疊上替換 (On-Stack Replacement,OSR)

在執行期間,Truffle 會排定「熱門」呼叫目標進行編譯。一旦目標編譯完成,後續對目標的呼叫可以執行編譯的版本。然而,正在進行的呼叫目標執行將不會從此編譯中受益,因為它無法將執行傳輸到編譯的程式碼。這表示長時間執行的目標可能會「卡在」解譯器中,損害暖機效能。

堆疊上替換 (OSR) 是 Truffle 中用於「脫離」解譯器,將執行從解譯程式碼轉移到編譯程式碼的一種技術。Truffle 支援 AST 解譯器 (例如,具有 LoopNode 的 AST) 和位元組碼解譯器 (例如,具有調度迴圈的節點) 的 OSR。在任一情況下,Truffle 都會使用啟發式方法來偵測何時正在解譯長時間執行的迴圈,並且可以執行 OSR 來加速執行。

針對 AST 解譯器的 OSR #

使用標準 Truffle API 的語言可以在 Graal 上免費獲得 OSR。執行階段會追蹤 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 介面。此介面至少需要三個方法實作

  • executeOSR(osrFrame, target, interpreterState):此方法使用 osrFrame 作為目前的程式狀態,將執行分派到指定的 target (即位元組碼索引)。interpreterState 物件可以傳遞恢復執行所需的任何其他解譯器狀態。
  • getOSRMetadata()setOSRMetadata(osrMetadata):這些方法會代理對類別上宣告的欄位的存取。執行階段將使用這些存取器來維護與 OSR 編譯相關的狀態 (例如,反向邊緣計數)。此欄位應標註 @CompilationFinal

在主要的調度迴圈中,當語言遇到反向邊緣時,應叫用提供的 BytecodeOSRNode.pollOSRBackEdge(osrNode) 方法,以通知執行階段反向邊緣。如果執行階段認為節點符合 OSR 編譯的資格,則此方法會傳回 true

如果 (而且只有在) pollOSRBackEdge 傳回 true 時,語言可以呼叫 BytecodeOSRNode.tryOSR(osrNode, target, interpreterState, beforeTransfer, parentFrame) 來嘗試 OSR。此方法會從 target 開始要求編譯,一旦有編譯的程式碼可用,後續呼叫可以透明地叫用編譯的程式碼,並傳回計算的結果。我們稍後將討論 interpreterStatebeforeTransfer 參數。

上述範例可以重構為支援 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 返回,就不需要在解譯器中繼續執行;結果可以直接轉發給呼叫者。

tryOSRinterpreterState 參數可以包含執行所需的任何其他解譯器狀態。此狀態會傳遞到 executeOSR,並且可以用來恢復執行。例如,如果解譯器使用資料指標來管理讀取/寫入,而且每個 target 的指標都是唯一的,則此指標可以傳遞到 interpreterState 中。編譯器可以看到此指標,並在部分評估中使用。

tryOSRbeforeTransfer 參數是一個可選的回呼,會在執行 OSR 之前叫用。由於 tryOSR 可能會或可能不會執行 OSR,此參數是在傳輸到 OSR 程式碼之前執行任何動作的一種方式。例如,語言可能會傳遞回呼,以便在跳到 OSR 程式碼之前傳送檢測事件。

BytecodeOSRNode 介面也包含一些可以使用預設實作覆寫的勾點方法

  • copyIntoOSRFrame(osrFrame, parentFrame, target)restoreParentFrame(osrFrame, parentFrame):在 OSR 程式碼中重複使用解譯的 Frame 並非最佳,因為它會逸出 OSR 呼叫目標,並且會防止純量取代 (如需有關純量取代的背景資訊,請參閱這篇論文)。如果可以,Truffle 會使用 copyIntoOSRFrame 將解譯的狀態 (parentFrame) 複製到 OSR Frame (osrFrame) 中,並使用 restoreParentFrame 在之後將狀態複製回父 Frame 中。根據預設,這兩個勾點都會在來源框架和目標框架之間複製每個插槽,但是可以覆寫此行為以進行更精細的控制 (例如,僅複製作用中的變數)。如果覆寫,這些方法應該謹慎地撰寫,以支援純量取代。
  • prepareOSR(target):此勾點會在編譯 OSR 目標之前呼叫。它可以強制在編譯之前進行任何初始化。例如,如果只能在解譯器中初始化欄位,則 prepareOSR 可以確保欄位已初始化,如此 OSR 程式碼在嘗試存取它時不會取消最佳化。

實作基於位元組碼的 OSR 可能很棘手。一些除錯提示

  • 請確定 metadata 欄位已標記為 @CompilationFinal
  • 如果已先具體化具有指定 FrameDescriptorFrame,Truffle 會重複使用解譯器 Frame,而不是複製 (如果使用複製,任何現有具體化的 Frame 都可能與 OSR Frame 不同步)。
  • 追蹤編譯和取消最佳化記錄有助於識別可以在 prepareOSR 中完成的任何初始化工作。
  • 在 IGV 中檢查編譯的 OSR 目標有助於確保複製勾點能與部分評估良好互動。

如需更多詳細資訊,請參閱 BytecodeOSRNode javadoc

命令列選項 #

有兩個 (實驗性) 選項可以用來設定 OSR

  • 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 編譯的更多詳細資訊,請參閱 除錯

與我們聯絡