34 Graal编译器深度解析:用Java写的Java编译器,性能真的更强吗?

图片[1]-34 Graal编译器深度解析:用Java写的Java编译器,性能真的更强吗?-速优课

本文导读

你可能听说过 GraalVM——这个 Oracle Labs 推出的高性能多语言执行环境近年来越来越火。但你了解它的核心基石 Graal 编译器吗?这是一个用 Java 语言编写的即时编译器,从 Java 9 开始被集成进 JDK,作为实验性的 JIT 编译器。

本文将带你深入了解 Graal 编译器:它是如何与 JVM 交互的?它和传统的 C2 编译器有什么区别?它的内部实现又是怎样的?为什么用 Java 写编译器反而可能更快?

通过阅读本文,你将了解:

  • Graal 编译器与 JVM 的交互方式和 JVMCI 接口
  • Graal 与 C2 编译器的核心区别与性能对比
  • Graal 的编译架构:前端 IR 与后端 IR
  • Graal 的激进优化策略和假设机制
  • Graal 中 intrinsic 方法的实现方式

一、Graal 与 JVM 的交互

1.1 即时编译器的三大职责

我们知道,即时编译器是 JVM 中相对独立的模块,它的核心任务是把 Java 字节码编译成可以直接运行的机器码。

具体来说,即时编译器与 JVM 的交互主要体现在三个方面:

  1. 响应编译请求:JVM 判断某个方法足够”热”时,会向编译器发起编译请求
  2. 获取编译数据:编译器需要从 JVM 获取类、方法、字段等元数据,以及反映程序执行状态的 profile 数据
  3. 部署编译结果:编译器把生成的机器码部署到代码缓存(code cache)中

这三个功能构成了一个完整的编译周期:接收请求 → 获取数据 → 完成编译 → 部署结果。

1.2 JVMCI:解耦编译器与 JVM

传统上,即时编译器和 JVM 是紧耦合的——改编译器就得重新编译整个 JVM。这对于迭代非常活跃的 Graal 来说显然不可接受。

为了解耦,JVM 引入了 JVMCI(JVM Compiler Interface),把上面三个功能抽象成了 Java 层面的接口。这样一来,只要 JVMCI 版本不变,只需替换 Graal 相关的 jar 包(Java 9 以后是 jmod 文件)就能升级编译器,不需要动 JVM 本身。

Graal 编译器可以通过以下参数启用,它会替换掉 HotSpot 中的 C2 编译器:

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

1.3 JVMCI 的更多可能性

JVMCI 的作用不止是响应 JVM 发出的编译请求。实际上,Java 程序可以直接调用 Graal 来编译和部署指定的方法。

这项技术有很多应用场景:

  • 单元测试:测试某个优化是否生效时,不需要反复运行方法等它变热,直接指定编译该方法即可
  • Truffle 语言框架:我们下一篇要介绍的 Truffle 就是基于这项技术实现的

二、Graal vs C2:两大编译器的对决

2.1 语言之争:Java vs C++

Graal 和 C2 最直观的区别就是开发语言:

  • Graal:用 Java 写的
  • C2:用 C++ 写的

很多人第一反应可能是:用 C++ 写的编译器肯定更快吧?但实际上并非如此简单。

首先,当编译器充分预热后,Graal 自身的热点代码也会被即时编译成机器码,执行速度并不比静态编译的 C++ 差。

其次,就算 Graal 解释执行会慢一些,那也只是编译效率的问题,不影响编译结果的性能。换句话说,如果 C2 和 Graal 采用完全相同的优化手段,它们生成的机器码性能是一样的,程序达到稳定状态后的峰值性能也不会有差别。

用 Java 写编译器最大的优势是开发效率高、易维护。Graal 更加模块化,新功能迭代也更快。就连 C2 的作者 Cliff Click 都表示不想再用 C++ 开发 JVM 了。

2.2 优化能力的对比

由于 Java 语言更容易开发和维护,把 C2 的新优化移植到 Graal 中相对容易。但反过来就不那么容易了——比如 Graal 中的部分逃逸分析(Partial Escape Analysis)被证实非常有效,但至今没有移植到 C2 中。

另一个重要的优化分歧是方法内联算法。Graal 的内联算法对新语法、新语言更友好,比如 Java 8 的 Lambda 表达式,以及 Scala 语言。

那么实际性能对比如何呢?有人统计过数十个 Java 和 Scala 程序的峰值性能:

  • Java 程序:Graal 的优势不明显,和 C2 差不多或略好
  • Scala 程序:Graal 的性能优势达到了约 10%

大规模使用 Scala 的 Twitter 就在生产环境部署了 Graal 编译器,获得了约 11% 的性能提升(基于 GraalVM 社区版)。


三、Graal 的内部实现

3.1 编译架构:前端与后端

Graal 把编译过程分为前端后端两大部分:

部分主要职责
前端平台无关优化(如方法内联),以及少量平台相关优化
后端大部分平台相关优化(如寄存器分配),以及机器码生成

我们之前介绍过,Graal 和 C2 都采用了 Sea-of-Nodes IR。严格来说,这指的是 Graal 的前端 IR,也叫 HIR(High-level IR)。后端采用的是另一种非 Sea-of-Nodes 的 IR,叫 LIR(Low-level IR)

3.2 前端:一个个优化阶段组成的流水线

Graal 的前端是由一个个独立的优化阶段(Optimization Phase)构成的。你可以把每个优化阶段想象成一个图算法:输入一张图,遍历并优化图上的节点,输出优化后的图。

前端的大部分优化阶段都可以通过配置选项开启或关闭,只有少数关键阶段是必选的。

图片[2]-34 Graal编译器深度解析:用Java写的Java编译器,性能真的更强吗?-速优课

感兴趣的话可以去 Graal 源码里看配置这些优化阶段的文件:HighTier.javaMidTier.javaLowTier.java

3.3 激进的投机性优化

Graal 和 C2 都采用了激进的投机性优化(Speculative Optimization)

这类优化基于某种假设(Assumption)。如果假设成立,优化后的代码会更快;如果假设不成立,JVM 会通过去优化(Deoptimization)机制从编译执行切换回解释执行,必要时还会废弃这份机器码,重新收集 profile 后再编译。

举个经典的例子:类层次分析(CHA)。在做虚方法内联时,如果发现某个接口只有一个实现,就可以假设之后也只有这一个实现,直接内联。如果后来加载了第二个实现,就废弃掉之前的编译结果。

Graal 比 C2 更加激进。它从设计上就非常青睐基于假设的优化:

  • 支持自定义假设,直接与去优化节点关联
  • 当去优化触发时,JVM 会记录是哪个假设失败了
  • 第二次编译同一个方法时,Graal 就知道这个假设不成立,不会再用同样的激进优化

这种”吃一堑长一智”的机制,让 Graal 敢于做更多的激进优化,同时又能保证正确性。

3.4 Intrinsic 方法的实现

Intrinsic 方法是 JVM 中另一个重要的性能优化手段。之前我们专门有一篇文章介绍过。

在 Graal 中实现高性能的 intrinsic 方法相对简单。Graal 提供了一种方法替换机制:在解析 Java 字节码时,把匹配到的方法调用替换成对另一个内部方法的调用,或者直接替换成一个特殊节点。

举个例子,数组比较方法 java.util.Arrays.equals(byte[], byte[]) 可以被替换成一个特殊的 IR 节点,用一个节点来代表整个数组比较逻辑。这样做的好处是:

  • 当前编译方法对应的图被简化了
  • 更容易与其他优化(如循环优化、常量折叠)结合

总结与思考

本文要点回顾

  1. Graal 通过 JVMCI 与 JVM 交互
    • JVMCI 把编译器与 JVM 解耦,升级编译器无需重新编译 JVM
    • 三大交互:响应编译请求、获取编译数据、部署编译结果
    • Java 程序可以直接调用 Graal 编译指定方法
  2. Graal vs C2
    • Graal 用 Java 写,更易开发维护;C2 用 C++ 写,历史包袱重
    • 峰值性能取决于优化手段,与编译器开发语言无关
    • Graal 的优化更激进,部分逃逸分析等优化还没移植到 C2
    • 对 Scala 等语言,Graal 的性能优势更明显(约 10%)
  3. Graal 的编译架构
    • 前端 HIR:Sea-of-Nodes,平台无关优化为主
    • 后端 LIR:非 Sea-of-Nodes,平台相关优化和代码生成
    • 前端由一个个可配置的优化阶段组成
  4. 激进的投机性优化
    • Graal 支持自定义假设,与去优化节点直接关联
    • 去优化时记录失败的假设,二次编译时避开
    • 这种机制让 Graal 敢于做更多激进优化
  5. Intrinsic 方法:Graal 通过方法替换机制实现 intrinsic,可以直接替换为特殊 IR 节点,便于后续优化。

思考与实践

  1. 你觉得用 Java 编写 JIT 编译器还有哪些优势或劣势?
  2. 投机性优化的核心是”假设-验证”机制,在你的工作中有没有类似的设计思路?
  3. 动手试试启用 Graal 编译器,对比一下你的应用在 C2 和 Graal 下的性能差异。

值得注意的是,Graal 编译器本身也需要被即时编译,所以在程序刚开始运行时,它会抢占本应用于编译应用代码的计算资源,导致启动性能较差。这个问题我们会在最后一篇介绍 SubstrateVM 时给出解决方案。

© 版权声明
THE END
喜欢就支持一下吧
点赞7
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!