- 適用於 JDK 23 的 GraalVM (最新版)
- 適用於 JDK 24 的 GraalVM (搶先體驗版)
- 適用於 JDK 21 的 GraalVM
- 適用於 JDK 17 的 GraalVM
- 封存
- 開發組建
- Truffle 語言實作框架
- Truffle 分支檢測
- 動態物件模型
- 靜態物件模型
- 針對解譯器程式碼的主機最佳化
- Truffle 的函式內嵌方法
- 分析 Truffle 解譯器
- Truffle Interop 2.0
- 語言實作
- 使用 Truffle 實作新的語言
- Truffle 語言和工具遷移至 Java 模組
- Truffle 原生函式介面
- 最佳化 Truffle 解譯器
- 選項
- 堆疊上替換
- Truffle 字串指南
- 特製化直方圖
- 測試 DSL 特製化
- 基於 Polyglot API 的 TCK
- Truffle 的編譯佇列方法
- Truffle 程式庫指南
- Truffle AOT 概觀
- Truffle AOT 編譯
- 輔助引擎快取
- Truffle 語言安全點教學
- 單態化 (Monomorphization)
- 分割演算法
- 單態化使用案例
- 向執行階段報告多型特製化
堆疊上替換 (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
開始要求編譯,一旦有編譯的程式碼可用,後續呼叫可以透明地叫用編譯的程式碼,並傳回計算的結果。我們稍後將討論 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)
:在 OSR 程式碼中重複使用解譯的Frame
並非最佳,因為它會逸出 OSR 呼叫目標,並且會防止純量取代 (如需有關純量取代的背景資訊,請參閱這篇論文)。如果可以,Truffle 會使用copyIntoOSRFrame
將解譯的狀態 (parentFrame
) 複製到 OSRFrame
(osrFrame
) 中,並使用restoreParentFrame
在之後將狀態複製回父Frame
中。根據預設,這兩個勾點都會在來源框架和目標框架之間複製每個插槽,但是可以覆寫此行為以進行更精細的控制 (例如,僅複製作用中的變數)。如果覆寫,這些方法應該謹慎地撰寫,以支援純量取代。prepareOSR(target)
:此勾點會在編譯 OSR 目標之前呼叫。它可以強制在編譯之前進行任何初始化。例如,如果只能在解譯器中初始化欄位,則prepareOSR
可以確保欄位已初始化,如此 OSR 程式碼在嘗試存取它時不會取消最佳化。
實作基於位元組碼的 OSR 可能很棘手。一些除錯提示
- 請確定 metadata 欄位已標記為
@CompilationFinal
。 - 如果已先具體化具有指定
FrameDescriptor
的Frame
,Truffle 會重複使用解譯器Frame
,而不是複製 (如果使用複製,任何現有具體化的Frame
都可能與 OSRFrame
不同步)。 - 追蹤編譯和取消最佳化記錄有助於識別可以在
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 編譯的更多詳細資訊,請參閱 除錯。