【QCon】精华内容上线92%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

深入浅出 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:4418807

评论

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

IBM Storage Ceph:现代数据湖仓的理想技术底座

财见

WebSocket 教程:实现二进制数据传输的详细指南

Apifox

前端 后端 websocket 实时通信 二进制数据

Unity 现正式支持 visionOS 平台,赋能Apple Vision Pro应用创建

财见

火山引擎边缘云2023年度回顾,挑战与创新的交响乐章

火山引擎边缘云

边缘计算 火山引擎 火山引擎边缘云

新型生成式 AI 助手 Amazon Q(预览版)上线

亚马逊云科技 (Amazon Web Services)

人工智能 生成式人工智能

发布、部署,傻傻分不清楚?从概念到实际场景,再到工具应用,一篇文章让你彻底搞清楚

EquatorCoco

Java 大数据 前端 部署

Wireshark中的TCP协议包分析

小齐写代码

低代码平台如何快速开发应用?

互联网工科生

低代码开发 JNPF

跨语言高性能 RPC 框架 Focus 发布 V1.2.0

dinstone

RPC框架

TDengine 签约杭州云润,助力某大型水表企业时序数据处理

TDengine

tdengine 时序数据库

云服务器是干什么的?其工作原理及优势解析

天翼云开发者社区

云计算 云服务器

深入剖析Java中的反射,由浅入深,层层剥离!

不在线第一只蜗牛

Java 编程 前端 开发语言

Pixillion pro for Mac(好用的图像转换软件) v12.13注册激活版

影影绰绰一往直前

专科学历被裁,我是如何逆天改命的?

霍格沃兹测试开发学社

2023 OpenHarmony年度运营报告

OpenHarmony开发者

OpenHarmony

面试官:你能简单聊聊MyBatis执行流程

华为云开发者联盟

Java 开发 华为云 华为云开发者联盟

职场逆袭:被裁成为薪资翻倍的契机

测试人

软件测试

云MySQL 主节点故障

腾讯云混沌演练平台

混沌工程 MySQL主节点故障

重构商业生态:支付宝小程序云与AI融合的无限可能

TRaaS

小程序 支付宝小程序 AI 小程序云

游戏开发巨擘的选择:2023 TGA获奖工作室共同青睐Perforce版本控制

龙智—DevSecOps解决方案

游戏开发 游戏 TGA

京东广告算法架构体系建设--大规模稀疏场景高性能训练方案演变

京东科技开发者

2023年哪个前端框架用的最多?

伤感汤姆布利柏

专科逆袭!裁员后薪资翻倍,他的成功秘诀竟然是…

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

测试

Gartner 2023 API管理魔力象限发布,SmartBear被评为“远见者”

龙智—DevSecOps解决方案

Gartner SmartBear

演讲回顾:如何为大规模研发团队加速CI构建,实现高效流水线

龙智—DevSecOps解决方案

DevSecOps Atlassian

Ray on ACK 实践探索之旅 - RayCluster 篇

阿里巴巴云原生

阿里云 云原生 ray

【鸿蒙千帆起】高德地图携手HarmonyOS NEXT,开启智能出行新篇章

HarmonyOS开发者

HarmonyOS

Atlassian Intelligence功能简介,AI驱动生成、概括、优化内容,助力工作效率加倍

龙智—DevSecOps解决方案

AI Atlassian

代码可控是什么意思?有哪些优势?

行云管家

信创 国产化 代码可控

云电脑超详细技术讲解

天翼云开发者社区

云计算 云电脑

Aetina发布首款采用NVIDIA Ada Lovelace架构的MXM图形模块

财见

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