JEP 515: 提前编译时方法剖析 (Ahead-of-Time Method Profiling)

作者| Igor Veresov & John Rose ---|--- 负责人| John Rose 类型| 功能特性 (Feature) 范围| 实现 (Implementation) 状态| 候选 (Candidate) 组件| hotspot / compiler 讨论| leyden dash dev at openjdk dot org 工作量| M 持续时间| M 相关| JEP 483: 提前编译时类加载与链接 (Ahead-of-Time Class Loading & Linking) 审核人| Alex Buckley, Dan Heidinga, Vladimir Kozlov 创建时间| 2024/02/01 20:40 更新时间| 2025/05/08 20:27 问题| 8325147

摘要

通过使应用程序先前运行的方法执行配置文件在 HotSpot Java Virtual Machine 启动时立即可用,从而缩短预热时间。 这将使 JIT 编译器能够在应用程序启动后立即生成本机代码,而无需等待收集配置文件。

目标

动机

要真正了解应用程序的作用,我们必须运行它。 我们可以通过检查应用程序的源代码或其类文件来得出关于应用程序行为的简单结论,但我们不确定它如何与高度动态的 Java Platform 交互。 造成这种不确定性的一个原因是,在没有 final 或 sealed 修饰符的情况下,任何类都可以在任何时候被子类化,因此一个方法可以被多次调用,然后被覆盖并且永远不再被调用。 另一个原因是,可以加载新类以响应外部输入,以扩展应用程序的行为,即使其作者也无法预测。 静态分析总是可以被程序复杂性击败。 当运行应用程序时,JVM 可以识别哪些方法在执行重要的工作,以及它们如何执行。 为了使应用程序达到最佳性能,JVM 的即时编译器 (JIT) 必须找到不可预测的 热点 (hot) 方法,即那些消耗最多 CPU 时间的方法,并将它们的字节码编译为本机代码。 (因此得名“HotSpot JVM”。)由于先前的应用程序行为是未来行为的极好预测指标,因此先前行为的摘要可以将 JVM 的编译工作集中在真正重要的代码上。 自 JDK 1.2 以来,HotSpot JVM 已自动以 配置文件 (profiles) 的形式收集此摘要。 对于任何给定的方法,配置文件都会统计许多有用的事件,例如,其字节码指令被执行的次数以及遇到的对象类型。 拥有足够的配置文件数据后,JVM 具有统计基础来预测该方法的未来行为,从而为该方法生成优化的代码。 配置文件允许 JVM 优化热点方法并避免优化冷门方法; 这两个条件对于最佳性能都是必要的。 不幸的是,存在一个先有鸡还是先有蛋的问题:应用程序在预测其方法行为之前无法达到最佳性能,并且在应用程序运行相当长的时间之前无法预测方法行为。 JVM 当前通过在应用程序运行的早期阶段投入一些资源来收集配置文件来解决此问题。 在此 预热期 (warmup period) 间,应用程序运行速度较慢,直到 JIT 可以将热点方法编译为本机代码。 预热后,除非应用程序更改其行为模式,否则无需编译更多方法,从而触发新的预热期。 我们可以通过更早地在应用程序的 训练运行 (training run) 中收集配置文件来缩短预热时间。 这将分析和预测行为的工作从应用程序的生产生命周期中转移出来。 结果,应用程序在生产中的预热时间将仅由 JIT 编译的成本决定,并且应用程序可以更快地达到最佳性能。

描述

我们扩展了由 JEP 483 引入的 AOT cache,以在训练运行期间收集方法配置文件。 正如 AOT cache 当前存储 JVM 否则需要在启动时加载和链接的类一样,AOT cache 现在也存储 JVM 否则需要在应用程序运行的早期阶段收集的方法配置文件。 因此,应用程序的生产运行启动速度更快,并且更快地达到最佳性能。 在训练运行期间缓存的配置文件不会阻止在生产运行期间进行额外的分析。 这至关重要,因为应用程序在生产中的行为可能与在训练中观察到的行为不同。 即使使用缓存的配置文件,HotSpot JVM 也会继续分析和优化应用程序的运行,融合了 AOT 配置文件、在线分析和 JIT 编译的优势。 缓存配置文件的最终效果是,JIT 运行得更早,并且更准确,使用配置文件来优化热点方法,以便应用程序体验到更短的预热期。 JIT 任务本质上是并行的,因此当有足够的硬件资源可用时,预热的实际时间可能很短。 例如,这是一个程序,虽然很短,但使用了 Stream API,因此导致加载了近 900 个 JDK 类。 大约 30 个热点方法以最高优化级别编译:

import java.util.*;
import java.util.stream.*;
public class HelloStreamWarmup {
  static String greeting(int n) {
    var words = List.of("Hello", "" + n, "world!");
    return words.stream()
      .filter(w -> !w.contains("0"))
      .collect(Collectors.joining(", "));
  }
  public static void main(String... args) {
    for (int i = 0; i < 100_000; i++)
      greeting(i);
    System.out.println(greeting(0)); // "Hello, world!"
  }
}

此程序在没有配置文件的 AOT cache 中运行需要 90 毫秒。 将配置文件收集到 AOT cache 后,它运行需要 73 毫秒 - 提高了 19%。 带有配置文件的 AOT cache 占用了额外的 250 KB,比没有配置文件的 AOT cache 多了约 2.5%。 像这样的短程序只有很短的预热期,但由于及时准确的 JIT 活动,使用缓存的配置文件,预热速度会更快。 更复杂和运行时间更长的程序也可能更快地预热,原因相同。

替代方案

如果一个应用程序是如此可预测,以至于我们可以提前将其热点方法编译为本机代码,并且这样做可以使其在没有进一步 JIT 活动的情况下达到最佳性能,那么这种 AOT 代码优于缓存配置文件。 我们打算在未来的工作中实现 AOT 编译。 然而,许多应用程序受益于 AOT 编译和 JIT 编译的结合,因为它们的行为无法被 AOT 编译器准确预测。 因此,缓存配置文件和缓存 AOT 代码不是相互对立的,并且会协同作用,为各种应用程序提供最佳性能。 一种部分 AOT 解决方案,即合理的 AOT 代码逐渐被更好优化的 JIT 代码取代,最终似乎是最好的解决方案。 JIT 最初可以避开应用程序,花时间根据最新的分析信息来获得最终代码。

测试

风险和假设

除了 JEP 483 中已经提到的风险之外,没有新的风险。 AOT cache 的基本假设仍然有效:训练运行被认为是观察结果的一个很好的来源,当通过 AOT cache 传递到生产运行时,将有益于该生产运行的性能。