写点什么

深入浅出 Java 10 的实验性 JIT 编译器 Graal

  • 2018-04-11
  • 本文字数:4027 字

    阅读完需:约 13 分钟

引言

对于大部分应用开发者来说,Java 编译器指的是 JDK 自带的 javac 指令。这一指令可将 Java 源程序编译成.class 文件,其中包含的代码格式我们称之为 Java bytecode(Java 字节码)。这种代码格式无法直接运行,但可以被不同平台 JVM 中的 interpreter 解释执行。由于 interpreter 效率低下,JVM 中的 JIT compiler(即时编译器)会在运行时有选择性地将运行次数较多的方法编译成二进制代码,直接运行在底层硬件上。Oracle 的 HotSpot VM 便附带两个用 C++ 实现的 JIT compiler:C1 及 C2。

与 interpreter,GC 等 JVM 的其他子系统相比,JIT compiler 并不依赖于诸如直接内存访问的底层语言特性。它可以看成一个输入 Java bytecode 输出二进制码的黑盒,其实现方式取决于开发者对开发效率,可维护性等的要求。Graal 是一个以 Java 为主要编程语言,面向 Java bytecode 的编译器。与用 C++ 实现的 C1 及 C2 相比,它的模块化更加明显,也更加容易维护。Graal 既可以作为动态编译器,在运行时编译热点方法;亦可以作为静态编译器,实现 AOT 编译。在 Java 10 中,Graal 作为试验性 JIT compiler 一同发布( JEP 317 )。这篇文章将介绍 Graal 在动态编译上的应用。有关静态编译,可查阅 JEP 295 Substrate VM

分层编译(tiered compilation

在介绍 Graal 前,我们先了解 HotSpot 中的 tiered compilation。前面提到,HotSpot 集成了两个 JIT compiler — C1 及 C2(或称为 Client 及 Server)。两者的区别在于,前者没有应用激进的优化技术,因为这些优化往往伴随着耗时较长的代码分析。因此,C1 的编译速度较快,而 C2 所编译的方法运行速度较快。在 Java 7 前,用户需根据自己的应用场景选择合适的 JIT compiler。举例来说,针对偏好高启动性能的 GUI 用户端程序则使用 C1,针对偏好高峰值性能的服务器端程序则使用 C2。

Java 7 引入了 tiered compilation 的概念,综合了 C1 的高启动性能及 C2 的高峰值性能。这两个 JIT compiler 以及 interpreter 将 HotSpot 的执行方式划分为五个级别:

  • level 0:interpreter 解释执行
  • level 1:C1 编译,无 profiling
  • level 2:C1 编译,仅方法及循环 back-edge 执行次数的 profiling
  • level 3:C1 编译,除 level 2 中的 profiling 外还包括 branch(针对分支跳转字节码)及 receiver type(针对成员方法调用或类检测,如 checkcast,instnaceof,aastore 字节码)的 profiling
  • level 4:C2 编译

其中,1 级和 4 级为接受状态 — 除非已编译的方法被 invalidated(通常在 deoptimization 中触发),否则 HotSpot 不会再发出该方法的编译请求。

上图列举了4 种编译模式(非全部)。通常情况下,一个方法先被解释执行(level 0),然后被C1 编译(level 3),再然后被得到profile 数据的C2 编译(level 4)。如果编译对象非常简单,虚拟机认为通过C1 编译或通过C2 编译并无区别,便会直接由C1 编译且不插入profiling 代码(level 1)。在C1 忙碌的情况下,interpreter 会触发profiling,而后方法会直接被C2 编译;在C2 忙碌的情况下,方法则会先由C1 编译并保持较少的profiling(level 2),以获取较高的执行效率(与3 级相比高30%)。

Graal 可替换 C2 成为 HotSpot 的顶层 JIT compiler,即上述 level 4。与 C2 相比,Graal 采用更加激进的优化方式,因此当程序达到稳定状态后,其执行效率(峰值性能)将更有优势。

早期的 Graal 同 C1 及 C2 一样,与 HotSpot 是紧耦合的。这意味着每次编译 Graal 均需重新编译 HotSpot。 JEP 243 将 Graal 中依赖于 HotSpot 的代码分离出来,形成 Java-Level JVM Compiler Interface(JVMCI)。该接口主要提供如下三种功能:

  • 响应 HotSpot 的编译请求,并分发给 Java-Level JIT compiler
  • 允许 Java-Level JIT compiler 访问 HotSpot 中与 JIT compilation 相关的数据结构,包括类,字段,方法及其 profiling 数据等,并提供这些数据结构在 Java 层面的抽象
  • 提供 HotSpot codecache 的 Java 抽象,允许 Java-Level JIT compiler 部署编译完成的二进制代码

综合利用这三种功能,我们可以将 Java-Level 编译器(不局限于 Graal)集成至 HotSpot 中,响应 HotSpot 发出的 level 4 的编译请求并将编译后的二进制代码部署到 HotSpot 的 codecache 中。此外,单独利用上述第三种功能可以绕开 HotSpot 的编译系统 — Java-Level 编译器将作为上层应用的类库直接部署编译后的二进制代码。Graal 自身的单元测试便是依赖于直接部署而非等待 HotSpot 发出编译请求; Truffle 亦是通过此机制部署编译后的语言解释器。

Graal v.s. C2

前面提到,JIT Compiler 并不依赖于底层语言特性,它仅仅是一种代码形式到另一种代码形式的转换。因此,理论上任意 C2 中以 C++ 实现的优化均可以在 Graal 中通过 Java 实现,反之亦然。事实上,许多 C2 中实现的优化均被移植到 Graal 中,如近期由其他开发者贡献的 String.compareTointrinsic 的移植。当然,局限于 C++ 的开发 / 维护难度(个人猜测),许多 Graal 中被证明有效的优化并没有被成功移植到 C2 上,这其中就包含 Graal 的 inlining 算法及 partial escape analysis(PEA)。

Inlining 是指在编译时识别 callsite 的目标方法,将其方法体纳入编译范围并用其返回结果替换原 callsite。最简单直观的例子便是 Java 中常见的 getter/setter 方法 — inlining 可以将一个方法中调用 getter/setter 的 callsite 优化成单一内存访问指令。Inlining 被业内戏称为优化之母,其原因在于它能引发更多优化。然而在实践中我们往往受制于编译单元大小或编译时间的限制,无法无限制地递归 inline。因此,inlining 的算法及策略很大程度上决定了编译器的优劣,尤其是在使用 Java 8 的 stream API 或使用 Scala 语言的场景下。这两种场景对应的 Java bytecode 包含大量的多层单方法调用。

Graal 拥有两个 inliner 实现。社区版的 inliner 采用的是深度优先的搜索方式,在分析某一方法时,一旦遇到不值得 inline 的 callsite 时便回溯至该方法的调用者。Graal 允许自定义策略以判断某一 callsite 值不值得 inline。默认情况下,Graal 会采取一种相对贪婪的策略,根据 callsite 的目标方法的大小做出相应的决定。Graal enterprise 的 inliner 则对所有 callsite 进行加权排序,其加权算法取决于目标方法的大小以及可能引发的优化。当目标方法被 inline 后,其包含的 callsite 同样会进入该加权队列中。这两种搜索方式都较为适合拥有多层单方法调用的应用场景。

Escape analysis(逃逸分析,EA)是一类识别对象动态范围的程序分析。编译器中常见的应用有两类:如果对象仅被单一线程访问,则可去除针对该对象的锁操作;如果对象为堆分配且仅被单一方法访问(inlining 的重要性再次体现),则可将该对象转化成栈分配。后者通常伴随着 scalar replacement,即将对对象字段的访问替换成对虚拟局部操作数的访问,从而进一步将对象由栈分配转换成虚拟分配。这不仅节省了原本用于存放对象 header 的内存空间,而且可以在 register allocator 的帮助下将(部分)对象字段存放在寄存器中,在节省内存的同时提高执行效率(内存访问转换成寄存器访问)。

Java 中常见的 for-each loop 是 EA 的一大目标客户。我们知道 for-each loop 会调用被遍历对象的 iterator 方法,返回一个实现 interface Iterator 的对象,并利用其 hasNext 及 next 接口进行遍历。Java collections 中的容器类(如 ArrayList)通常会构造一个新的 Iterator 实例,其生命周期局限于该 for-each loop 中。如若 Iterator 实例的构造函数以及 hasNext,next 方法调用(连同它们方法体中以 this 为 receiver 的方法调用,如 checkForComodification())都被 inline,EA 会认为该实例没有逃逸,并采取栈分配及 scalar replacement。

理想情况下,Foo.bar 会被优化成如下代码:

HotSpot 的 C2 便已应用控制流无关的 EA 实现 scalar replacement。而 Graal 的 PEA 则在此基础上引入了控制流信息,将所有的堆分配操作虚拟化,并仅在对象确定逃逸的分支 materialize。与 C2 的 EA 相比,PEA 分析效率较低,但能够在对象没有逃逸的分支上实现 scalar replacement。如下例所示,如果 then-branch 的执行概率为 1%,那么被 PEA 优化后的代码在 99% 的情况下并不会执行堆分配,而 C2 的 EA 则 100% 会执行堆分配。另一个典型的例子是渲染引擎 Sunflow — 在运行 DaCapo benchmark suite 所附带的默认 workload 时,Graal 的 PEA 判定约 27% 的堆分配(共占 700M)可被虚拟化。该数字远超 C2 的 EA。

使用 Graal

在Java 10 (Linux/x64, macOS/x64) 中,默认情况下HotSpot 仍使用C2,但通过向java 命令添加-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 参数便可将C2 替换成Graal。

Oracle Labs GraalVM 是由 Oracle Labs 直接发布的 JDK 版本。它基于 Java 8,并且囊括了 Graal enterprise。如果对源代码感兴趣,可直接签出 Graal 社区版的 GitHub repo 。源代码的编译需借助 mx 工具及 labsjdk (注:请下载页面最下方的 labsjdk,直接使用 GraalVM 可能会导致编译问题)。

在 graal/compiler 目录下使用 mx eclipseinit,mx intellijinit 或 mx netbeansinit 可分别生成 Eclipse,IntelliJ 或 NetBeans 的工程配置文件。

参考链

Upcoming: advanced topics in Graal compiler

  • Debugging compiled code
  • Deoptimization & Java-level assumption — SpeculationLog
  • Graal method substitution & Snippet
  • Implementing JVM intrinsics

作者介绍:郑雨迪,现于 Oracle Labs 任职高级研究员,是 Graal 编译器组的核心开发者之一。他的研究方向包括动态编译及程序分析。在加入 Oracle Labs 前,郑雨迪于瑞士卢加诺大学攻读并获得博士学位。他即将在 QCon 北京 2018 现场分享《GraalVM 及其生态系统》,敬请关注。

2018-04-11 17:4419744

评论

发布
暂无评论
发现更多内容

TDengine与中泰证券正式签约,打造金融量化交易场景解决方案

TDengine

数据库 tdengine 时序数据库

GitHub标星已达26K+,鹅厂技术总监手写分布式架构体系笔记

小小怪下士

Java 程序员 分布式

调试3D渲染和3D可视化的五个好处

3DCAT实时渲染

可视化 3D渲染 云渲染 实时渲染

AI与低代码的结合及应用

力软低代码开发平台

WhaleDI数据治理利器之“低成本数据质量管理”

鲸品堂

12 月 PK 榜

在霍格沃兹测试开发学社学习是种怎样的体验?

测吧(北京)科技有限公司

测试

【电路设计】避开元器件的这些“坑”,多年的心梗都治好了!

华秋PCB

工具 PCB PCB设计

Transformer:让ChatGPT站在肩膀上的巨人?

Baihai IDP

AI Transformer

数据治理体系建设与数据资产路线图规划

用友BIP

前端程序员培训学习需要学多久?

小谷哥

皮皮App发起爱心捐赠,让城市里的荧荧之光,给乡村孩子带去一片暖冬

联营汇聚

为什么数字化时代需要 BizDevOps?

阿里云云效

DevOps 数字化转型 数字化 BizDevOps

Spring中11个最常用的扩展点,你知道几个?

JAVA旭阳

Java spring

如何在小程序中完成支付进件

Towify

微信小程序 编辑器 无代码

大数据开发技术培训班怎么选

小谷哥

前端编程学习有没有必要参加?

小谷哥

行业认可|墨菲安全登信息通信软件供应链安全社区优秀榜单

墨菲安全

参加java培训对学习程序员有用吗?

小谷哥

YonBuilder移动开发平台 AVM框架 封装省市区级联选择弹框

YonBuilder低代码开发平台

开发者 AVM

一文带你了解EiPaaS和EiPaaS的国际趋势

华为云开发者联盟

云计算 后端 华为云 12 月 PK 榜

阿里二面被问MySQL的事务隔离级别,结果回去等通知了

程序员小毕

MySQL 数据库 程序员 面试 后端

学习大数据培训和自学哪个比较好

小谷哥

“2022混合云TOP50”重磅发布 天翼云问鼎榜首

极客天地

游戏引擎中的实时渲染和在V-Ray中渲染有什么区别?

3DCAT实时渲染

渲染引擎 游戏引擎 渲染服务 游戏开发引擎

建筑、工程和施工产业中的3D可视化

3DCAT实时渲染

可视化 云渲染 实时云渲染 云渲染平台

一文读懂于Zebec生态中的潜在收益方式

鳄鱼视界

如何通过 NFTScan 发掘 NFT 项目的内在价值

NFT Research

区块链 NFT

第五届“强网”拟态防御国际精英挑战赛精彩落幕!——网络空间安全大赛再立新标杆

科技热闻

HIFIVE音加加提供曲库、评分、修音功能的K歌SDK-Android版本

数到3变暖男i

API 社交泛娱乐 娱乐社交 K歌 K歌SDK

OSCS开源安全周报第22期:NuGet 仓库中被发现 13.5 万个包含钓鱼地址的组件包

墨菲安全

MegEngine Windows Python wheel 包减肥之路

MegEngineBot

深度学习 开源 MegEngine

深入浅出Java 10的实验性JIT编译器Graal_语言 & 开发_郑雨迪_InfoQ精选文章