Graal 编译器
GraalVM 是一个高性能的、支持多种编程语言的执行环境。它既可以在传统的 OpenJDK 上运行,也可以通过 AOT(Ahead-Of-Time)编译成可执行文件单独运行,甚至可以集成至数据库中运行
- Graal 编译器:GraalVM 的基石
- 用 Java 写就的即时编译器,它从 Java 9u 开始便被集成自 JDK 中,作为实验性质的即时编译器
- 可以通过 Java 虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler启用
- 当启用时,它将替换掉 HotSpot 中的 C2 编译器,并响应原本由 C2 负责的编译请求
Graal 和 Java 虚拟机的交互
- 即时编译器是 Java 虚拟机中相对独立的模块
- 负责接收 Java 字节码
- 生成可以直接运行的二进制码
- 即时编译器与 Java 虚拟机的交互可以分为:
- 响应编译请求;
- 获取编译所需的元数据(如类、方法、字段)和反映程序执行状态的 profile
- 将生成的二进制码部署至代码缓存(code cache)里
- 即时编译器通过这三个功能组成了一个响应编译请求、获取编译所需的数据,完成编译并部署的完整编译周期
- 传统情况下,即时编译器是与 Java 虚拟机紧耦合的
- 为了让 Java 虚拟机与 Graal 解耦合,引入了Java 虚拟机编译器接口(JVM Compiler Interface,JVMCI),上述三个功能抽象成一个 Java 层面的接口
- 在 Graal 所依赖的 JVMCI 版本不变的情况下,我们仅需要替换 Graal 编译器相关的 jar 包(Java 9 以后的 jmod 文件),便可完成对 Graal 的升级
- JVMCI 的作用并不局限于完成由 Java 虚拟机发出的编译请求。Java 程序可以直接调用 Graal,编译并部署指定方法
- Graal 的单元测试便是基于这项技术
Graal 和 C2 的区别
- Graal 是用 Java 写的,而 C2 是用 C++ 写的
- Graal 更加模块化,也更容易开发与维护
- 在充分预热的情况下,Java 程序中的热点代码早已经通过即时编译转换为二进制码,在执行速度上并不亚于静态编译的 C++ 程序
- 即便是解释执行 Graal,也仅是会减慢编译效率,而并不影响编译结果的性能
- 如果 C2 和 Graal 采用相同的优化手段,那么它们的编译结果是一样的
- Graal 和 C2 另一个优化上的分歧则是方法内联算法。相对来说,Graal 的内联算法对新语法、新语言更加友好,例如 Java 8 的 lambda 表达式以及 Scala 语言
- 总体而言,Graal 编译结果的性能要优于 C2。对于 Java 程序来说,Graal 的优势并不明显;对于 Scala 程序来说,Graal 的性能优势达到了 10%
Graal 的实现
- Graal 编译器将编译过程分为前端和后端两大部分
- 前端用于实现平台无关的优化(如方法内联),以及小部分平台相关的优化
- 后端则负责大部分的平台相关优化(如寄存器分配),以及机器码的生成
- Graal 和 C2 都采用了 Sea-of-Nodes IR
- Graal 的前端是由一个个单独的优化阶段(optimization phase)构成的
- 我们可以将每个优化阶段想象成一个图算法:它会接收一个规则的图,遍历图上的节点并做出优化,并且返回另一个规则的图
- 前端中的编译阶段除了少数几个关键的之外,其余均可以通过配置选项来开启或关闭
- Graal 和 C2 都采用了激进的投机性优化手段(speculative optimization)
- Graal 与 C2 相比会更加激进。它从设计上便十分青睐这种基于假设的优化手段。在编译过程中,Graal 支持自定义假设,并且直接与去优化节点相关联