- 适用于 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)复制到 OSR- Frame(- 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 编译的更多详细信息,请参阅调试。