低延迟垃圾收集器中安全点附加屏障的吞吐量分析
2023 年 4 月 3 日此处展示的工作是作为 甲骨文、乌普萨拉大学和 KTH 之间的联合研究项目 的一部分进行的。关注 inside.java 上的博客系列,以阅读更多关于在斯德哥尔摩甲骨文开发办公室进行的 JVM 研究的信息。
我的名字是 Filip Wilén,我在 2022 年秋季在甲骨文撰写了我的硕士论文。这篇论文是在与甲骨文斯德哥尔摩办公室合作下进行的。我要对 Roberto Castañeda Lozano 和甲骨文团队在整个过程中的经验和指导表示最深切的感谢。
我的硕士论文对一项名为安全点附加屏障 (SAB) 的提议优化进行了吞吐量评估,该优化适用于 OpenJDK Hotspot 的并发垃圾收集器世代 ZGC。我的论文的主要任务是回答该优化是否产生了任何重大的吞吐量优势,以及找到该优化的主要吞吐量限制。然后,可以使用此知识来决定是否应在 ZGC 的发布版本中实现 SAB。全文可在此处找到 此处。
背景
并发内存管理背后的理念是同时运行应用程序线程和垃圾收集器线程。但是,由于应用程序和垃圾收集器同时访问 Java 堆,这可能会导致指针引用错误的内存字段。为了解决此问题,Generational ZGC 为每个存储和加载操作使用屏障,这些屏障会检查指针的引用并根据需要将其还原为正确的地址。不幸的是,这些屏障会引入开销,但理论上,只有在堆被重新排列时才需要这些屏障。因此,有人建议使用名为“安全点附加屏障”的 JIT 编译器优化来减少程序中可以确保堆稳定的部分中的屏障数量。作为一个简化的示例,如果算法可以确保已删除屏障的内存字段之前已由另一个屏障检查过,则 SAB 可以确保堆的完整性并删除屏障。这会导致删除不必要的屏障,从而提高吞吐量。
安全点附加屏障的预期吞吐量优势
在 SPECjvm2008 [1] 和 DaCapo2009 版本 9.12-bach-MR1 [2] 基准套件上评估了该优化。图 1 显示了 DaCapo 基准套件中基准的归一化执行时间。从 SPECjvm 和 DaCapo 中,只有 4/21 个基准显示出具有统计学意义的中值吞吐量提升。基准fop-default 的提升最佳,执行时间减少了约 1%。此外,其他 17/21 个基准未显示出明显的回归。这意味着 SAB 不会导致任何明显的吞吐量限制,但仅对某些特定基准激活具有统计学意义的吞吐量提升。

图 1:启用和禁用 SAB 时不同 DaCapo 基准的相对吞吐量结果的小提琴图。每个基准执行 50 次,每个基准的测量运行时间在 1-10 秒之间。
计数障碍
SAB 优化带来的吞吐量优势取决于消除的障碍数量。障碍可以静态和动态计数。静态计数意味着我们在代码中每次遇到障碍时就进行计数,而动态计数意味着在运行时每次到达障碍时就进行计数。
要计数障碍,必须在 JVM 中实现一个检测工具。借助此工具,可以确定对于大多数基准测试,通常可以消除 10 - 20% 的静态障碍。但是,消除的动态障碍数量通常要低得多,这表明从重复代码中消除障碍存在局限性。图 2 显示了与从 DaCapo 和 SPECjvm 基准套件中消除的中值动态障碍的百分比相关的运行时减少。从这个散点图中,运行时减少和消除的动态障碍之间似乎存在相关性。

图 2:从 DaCapo 和 SPEC 基准中消除的动态障碍与吞吐量提升之间的相关性。没有障碍计数相差超过 15%。
ZGC 中附加安全点的障碍的局限性
通过进一步评估 DaCapo 基准套件中的动态障碍消除率,可以发现两个主要的 SAB 局限性。重复代码段中障碍消除的局限性是由内联和 JIT 编译器执行的静态分析造成的。
内联局限性
内联意味着用调用的主体替换方法调用。在编译期间,所有跟踪的障碍都驻留在同一个编译单元中,这意味着选择了一个方法(根方法)并将一些进一步的方法调用内联到此方法中。这些都是可以在当前编译单元中跟踪的障碍。这限制了可以消除多少障碍以及可以消除哪些障碍,因为 JIT 编译器对可以内联的代码量有内部限制。对于 SAB,此限制导致总体消除的障碍更少,但对于重复代码段尤其如此。
考虑到这些局限性,改进障碍消除的一个简单解决方案可能是增加编译器设置的内联限制。图 3 在 DaCapo 基准套件上对此进行了测试,其中每个数据点都是具有不同内联限制的相同基准。限制通过方法字节码大小(要内联的方法的最大字节码大小)和内联最大字节大小(带有内联方法的方法的最大大小)增加。但是,如图 3 所示,这似乎不是一个明显的解决方案,因为大多数基准在动态消除的障碍数量上都没有显着增加。但相反,luindex-default 基准表明增强内联决策可能是有益的。

图 3:SAB 加载障碍消除在 DaCapo 基准中取决于内联限制。每个数据点都是 5 次运行的中值计数,其中消除的障碍的偏差小于 10%。
静态分析局限性
另一个局限性涉及 JIT 编译器为消除障碍而执行的静态分析。此限制由清单 1 说明,其中编译器只看到一个障碍,即使它被执行多次。这导致大量障碍被不必要地执行。
//Limitation
while(i < 100) {
//Load barrier cannot be removed
obj load = obj.l;
i++;
}
清单 1:即使障碍被执行多次,第 4 行的加载障碍也无法消除。
借助循环剥离(如清单 2 所示)可以解决此问题。通过将一次迭代移到主循环外部,可以消除循环内的屏障,从而有可能移除多个动态屏障。
// Solution with loop peeling
obj load = obj.l;
i++;
while(i < 100) {
//Load barrier removed!
obj load = obj.l;
i++;
}
清单 2:清单 1 中问题的解决方案是使用循环剥离,这会导致在主体内之前执行一次循环迭代。
结论
对于大多数基准,SAB 并未生成吞吐量增加,但当增加具有统计学意义时,增加幅度 < 1%。然而,我的论文表明算法存在可能的吞吐量改进,形式为循环剥离和增强的内联决策。希望这些新见解有助于决定是否在 ZGC 的发布版本中实现安全点附加屏障。
参考文献
[1] Specjvm® 2008,”标准性能评估公司 (SPEC),2017 年 8 月。[在线]。可用网址:https://www.spec.org/jvm2008/
[2] M. Blackburn 等,“DaCapo 基准:Java 基准开发和分析”,载于 OOPSLA ’06:第 21 届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序年度会议论文集。纽约,纽约,美国:ACM Press,2006 年 10 月。doi:https://doi.acm.org/10.1145/1167473.1167488 第 169–190 页。