- 适用于 JDK 24 的 GraalVM(最新)
- 适用于 JDK 25 的 GraalVM(早期访问)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发构建
- Truffle 语言实现框架
- Truffle 分支插桩
- 动态对象模型
- 静态对象模型
- 解释器代码的主机优化
- Truffle 函数内联方法
- 分析 Truffle 解释器
- Truffle 互操作 2.0
- 语言实现
- 使用 Truffle 实现新语言
- Truffle 语言和工具迁移到 Java 模块
- Truffle 原生函数接口
- 优化 Truffle 解释器
- 选项
- 栈上替换
- Truffle 字符串指南
- 特化直方图
- 测试 DSL 特化
- 基于多语言 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
并非最佳选择,因为它会逸出 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 编译的更多详细信息,请参阅调试。