深入拆解 Java 虚拟机
郑雨迪
Oracle 高级研究员,计算机博士
87446 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 40 讲
模块四:黑科技 (3讲)
深入拆解 Java 虚拟机
15
15
1.0x
00:00/00:00
登录|注册

18 | 即时编译器的中间表达形式

其他优化方式
冗余赋值识别
数据流分析
优化方式
特点
生成目标代码
IR优化
语义分析
语法分析
词法分析
与CSE的区别
实例
优化技术
内存依赖
节点调度
基本块
IR图
特点
Phi函数
静态单赋值(SSA)IR
Java字节码作为IR
后端处理
生成IR
前端处理
实践环节
基于IR的优化GVN
IGV可视化工具
Sea-of-Nodes的IR
即时编译器的内部构造
Global Value Numbering (GVN)
Sea-of-nodes
中间表达形式(IR)
总结与实践
即时编译器的中间表达形式

该思维导图由 AI 生成,仅供参考

在上一章中,我利用了程序控制流图以及伪代码,来展示即时编译器中基于 profile 的优化。不过,这并非实际的优化过程。

1. 中间表达形式(IR)

在编译原理课程中,我们通常将编译器分为前端和后端。其中,前端会对所输入的程序进行词法分析、语法分析、语义分析,然后生成中间表达形式,也就是 IR(Intermediate Representation )。后端会对 IR 进行优化,然后生成目标代码。
如果不考虑解释执行的话,从 Java 源代码到最终的机器码实际上经过了两轮编译:Java 编译器将 Java 源代码编译成 Java 字节码,而即时编译器则将 Java 字节码编译成机器码。
对于即时编译器来说,所输入的 Java 字节码剥离了很多高级的 Java 语法,而且其采用的基于栈的计算模型非常容易建模。因此,即时编译器并不需要重新进行词法分析、语法分析以及语义分析,而是直接将 Java 字节码作为一种 IR。
不过,Java 字节码本身并不适合直接作为可供优化的 IR。这是因为现代编译器一般采用静态单赋值(Static Single Assignment,SSA)IR。这种 IR 的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用。
y = 1;
y = 2;
x = y;
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

即时编译器中的中间表达形式(IR)对于编译器优化至关重要。Sea-of-Nodes类型的SSA IR去除了变量的概念,直接采用变量所指向的值进行运算,使得编译器可以进行各种优化,如常量折叠、常量传播、强度削减和死代码删除等。其中,Global Value Numbering(GVN)是一种发现并消除等价计算的优化技术,通过GVN可以将重复的计算合并,从而降低输出机器码的大小。在Sea-of-Nodes中,由于只存在值的概念,GVN算法变得非常简单。通过GVN的优化,可以在IR图上实现公共子表达式消除,进一步提高编译器的效率。整体而言,本文深入浅出地介绍了即时编译器中的中间表达形式及其优化过程,对于理解编译器优化具有重要参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《深入拆解 Java 虚拟机》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(20)

  • 最新
  • 精选
  • ext4
    除了你上面提到的内存依赖,我看到C2的ideal graph里面还有一种依赖叫做I/O dependency,这个在Graal的graph里似乎也没有了。可以解释一下C2的这个I/O dependency是做什么的,以及Graal是如何替代这种依赖的表示的么?

    作者回复: 首先这些dependency都是为了scheduling服务的,也就是把图序列化为指令序列,图节点中的相互依赖会让它们拥有先后关系。 个人理解哈,原本的IO dependency就是IO顺序,Cliff Click大神的论文中说的是没有内存映射的IO访问。现在C2应该用来泛指非内存访问的JVM状态。 Graal里这种额外的依赖关系都是用控制流先后顺序来表达的。

    2018-08-31
    7
  • Shine
    IR图有点看不懂。基本块是根据什么原则划分的? 有些块有start,begin, end等等,有些块却没有? 为什么GVN代码中,都是判断a,b是否大于0,图中B3来了一个Merge节点?

    作者回复: 先说一下,这个是Graal的IR。 基本块的划分规则是根据其定义来的,即最长的,顺序执行的节点集。 start begin end是为了标注基本块的起始和结尾,便于优化,没有其他实际意义。 merge节点可以看成控制流交汇的节点,在Graal里就是用来挂phi节点的。最后有两个return而没有merge,实际上是另外一个优化code duplicati,将原本的merge优化掉,生成两条独立程序路径。在这两条路径中,b的值是唯一的,而不是一个phi方法。

    2018-09-03
    4
  • Ken张云忠
    郑老师,本节中第一张IR图和下面的Control Flow图是使用http://ssw.jku.at/General/Staff/TW/igv.html的IdealGraphVisualizer查看的吧. 这个工具使用jdk7启动起来后,但是在执行时必须要使用debug版的jdk7才能执行参数-XX:PrintIdealGraphLevel=2 -XX:PrintIdealGraphFile=ideal.xml,一直困扰在获取不到debug版的jdk7,下载openjdk7自己编译过程中遇到了太多问题,尤其是build-debug/hotspot中太多代码编译不过去的问题. 老师是怎么样一步步得到debug版的jdk7的?

    作者回复: 那个比较老,你可以去下载GraalVM,里面有附带igv的。OpenJDK 7编译是比较难,很多奇奇怪怪的依赖问题,8之后就好多了

    2020-01-17
    3
  • likun
    你好 我这边找不到bebug版本的jdk10,好像无法查看ir图

    作者回复: 导出Graal IR不用debug版本的JDK

    2018-10-22
  • 夜行观星
    看懂这篇文章,已经是一年之后,时间真快
    2019-11-19
    6
    17
  • 搬砖的孟达
    看不太懂。哈哈哈...可能基础还不到这个水平吧,多看多思考吧。
    2018-12-09
    13
  • the geek
    这篇文章最好还是看懂,后面的方法内联章节会经常出现IR图,我一开始也是看了个大概,看了方法内联后,回来静下心一看,还是比较简单的 主要就是将方法的执行流程转换为IR图。 IR图中一些符号解释(以下是个人简单理解,仅供参考): 1. 常量值:C(0)、C(1)。就是常量值1、2 (类型是i32) 2. 参数值P(0)、P(1)。就是方法参数0和方法参数1=>上面int a,int b 3.Phi(IR节点1,IR节点2,内存类型)。(i32可能是说int 32位 ,方便分配内存吧?个人猜测老师指正)
    2020-02-05
    8
  • 房艳
    看了好几遍,边看边整理思路,看懂个大概。这是我边看边画的知识点: java源代码---java编译器--->java字节码---即时编译器--->机器码 编译器:前端:IR(词法分析、语法分析、语义分析--->生成中间表达形式) 后端:IR优化--->生成目标代码 即时编译器直接将java字节码作为一种IR,即时编译器将java字节码转为SSA IR(IR图) 静态单赋值IR(SSA IR):每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用 SSA IR存在的问题:不同执行路径会对同一个变量设置不同的值 解决方法:Phi函数-能够根据不同的执行路径选择不同的值 1.C2采用Sea-of-Nodes的SSA IR,去除变量的概念,直接采用变量所指向的值进行运算。 如:Phi(y1,y2) ---> Phi(1,0) C2没有固定节点的概念,所有的IR节点都是浮动节点。将根据各个基本块头尾之间的控制依赖,以及数据依赖和内存依赖,来进行节点调度。 节点调度:在编译过程中,编译器需要(多次)计算浮动节点具体的排布位置。这个过程称为节点调度。 内存依赖:假设一段程序往内存中存储了一个值,而后又读取同一内存,那么显然程序希望读取到的是所存储的值。 即时编译器不能任意调度对同一内存地址的读写,因为它们之间存在依赖关系。 C2的做法便是将这种时序上的先后记录为内存依赖,并让节点调度算法在进行调度时考虑这些内存依赖关系。 2.Graal的IR也是Sea-of-Nodes类型,可理解为是C2 IR的精简版本 Graal将内存读写转换成固定节点。由于固定节点存在先后关系,因此无需额外记录内存依赖。 IR的可视化工具 IGV:被控制流边所连接的是固定节点,其他的皆属于浮动节点。 IR图是竖着看,然后遇到if分支就会有两个流程(满足与不满足),hpi函数里面用了节点上的编号,我理解是这样。 GVN是一种发现并消除等价计算的优化技术。例如如果一段程序中出现多次相同的乘法,那么即时编译器可以将这些乘法合并为一个。 在 Sea-of-Nodes 中,由于只存在值的概念,因此 GVN 算法将非常简单: 如果一个浮动节点本身不存在内存副作用(由于 GVN 可能影响节点调度,如果有内存副作用的话,那么将引发一些源代码中不可能出现的情况) , 那么即时编译器只需判断该浮动节点是否与已存在的浮动节点的类型相同,所输入的 IR 节点是否一致,便可以将这两个浮动节点归并成一个。 有个问题:内存副作用是什么意思?有点不理解,麻烦帮我解答一下。
    2021-01-19
    4
  • neohope
    想问一下老师,idealgraphvisualizer中,有没有办法看到全局的IR图?开启后,好像有很多次优化,每次都只能看到一部分哦。 此外: 1、官网下载的idealgraphvisualizer是2011年版本,没法用,要用直接在github上下载的版本 2、idealgraphvisualizer当前版本,好像只支持JDK1.8? 3、从graal官网下载的版本只有JDK1.8,下载了也没有用。直接下载Oracle JDK11就可以了 4、最后例子的Demo,JDK11参数要调整一下: java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -XX:CompileCommand=dontinline,"CompilationTest.hash()" -Dgraal.Dump=:3 -Dgraal.OptDeoptimizationGrouping=false CompilationTest 5、前面两个例子,需要用Debug版本的JDK。最后一个不需要。
    2019-09-02
    3
    4
  • 鱼肚
    原本里的 IGV 用不了,用这个 https://github.com/oracle/graal/releases/tag/idealgraphvisualizer-543
    2019-09-03
    3
收起评论
显示
设置
留言
20
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部