- 适用于 JDK 23 的 GraalVM(最新)
- 适用于 JDK 24 的 GraalVM(抢先体验)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发版本
- Truffle 语言实现框架
- Truffle 分支仪器
- 动态对象模型
- 静态对象模型
- 解释器代码的主机优化
- Truffle 对函数内联的方法
- 分析 Truffle 解释器
- Truffle 交互 2.0
- 语言实现
- 使用 Truffle 实现新的语言
- Truffle 语言和仪器迁移到 Java 模块
- Truffle 本地函数接口
- 优化 Truffle 解释器
- 选项
- 栈上替换
- Truffle 字符串指南
- 专业化直方图
- 测试 DSL 专业化
- 基于 Polyglot API 的 TCK
- Truffle 对编译队列的方法
- Truffle 库指南
- Truffle AOT 概述
- Truffle AOT 编译
- 辅助引擎缓存
- Truffle 语言安全点教程
- 单态化
- 分割算法
- 单态化用例
- 向运行时报告多态专业化
栈上替换 (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
并不是最优的,因为这样会导致Frame
逃逸出 OSR 调用目标,并阻止标量替换(有关标量替换的背景信息,请参阅 这篇论文)。如果可能,Truffle 会使用copyIntoOSRFrame
将解释后的状态 (parentFrame
) 复制到 OSRFrame
(osrFrame
) 中,并使用restoreParentFrame
将状态复制回父Frame
。默认情况下,这两个挂钩都会在源帧和目标帧之间复制每个插槽,但可以将其覆盖以进行更精细的控制(例如,仅复制活动变量)。如果覆盖,则应仔细编写这些方法以支持标量替换。prepareOSR(target)
:此挂钩在编译 OSR 目标之前被调用。它可用于强制在编译之前执行任何初始化操作。例如,如果一个字段只能在解释器中初始化,prepareOSR
可以确保它被初始化,这样 OSR 代码在尝试访问它时不会进行反优化。
基于字节码的 OSR 可能难以实现。一些调试技巧
- 确保元数据字段使用
@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 编译的更多详细信息,请参阅 调试。