从HIR到LIR

LIR类似于三操作数的实现,但多了一些诸如对象分配和加锁的高级指令。C1遍历HIR的每个基本块,为每个基本块的每条SSA指令生成对应的LIR指令。从HIR到LIR的转换过程由LIRGenerator完成,如代码清单8-15所示。

代码清单8-15 HIRLIR

void LIRGenerator::block_do(BlockBegin* block) {
block_do_prolog(block); // 转换每个基本块前的操作
set_block(block);
// 遍历基本块中的所有HIR指令,调用do_root(instr)将它们转换为LIR指令
for (Instruction* instr = block; instr != NULL; instr = instr->next()) {
if (instr->is_pinned()) do_root(instr);
}
set_block(NULL);
block_do_epilog(block); // 转换每个基本块后的操作
}

do_root(instr)负责根据HIR生成LIR指令,但是生成的前提是HIR指令必须是经过pin处理的。假设HIR存在三条加法指令(i1:5,i2:5,i3:i1+i2),经过pin处理的指令会被编译器视作root,这里i3被pin处理,i1和i2作为常量未被pin处理,所以生成LIR时会跳过i1和i2直接从i3开始。

return生成

pin只是一个优化动作,即使未被pin住,只要有需要,编译器还是会为它生成对应的LIR。比如当处理i3时,编译器需要将i2、i3作为加法指令的操作数,此时它会使用LIRItem包装i2和i3两个操作数,并调用walk()为它们生成对应的LIR。生成LIR的过程如代码清单8-16所示。

代码清单8-16 LIR代码生成

void LIRGenerator::do_Return(Return* x) {
// 如果返回类型为void,则生成不包含操作数的return LIR指令
if (x->type()->is_void()) {
__ return_op(LIR_OprFact::illegalOpr);
} else {
// 否则为操作数创建虚拟寄存器,然后将虚拟机寄存器作为return指令的操作数
LIR_Opr reg = result_register_for(x->type(), /*callee=*/true);
LIRItem result(x->result(), this);
result.load_item_force(reg);
__ return_op(result.result());
}
set_no_result(x);
}

根据HIR的return是否返回void选择生成无操作数还是含一个操作数的LIR指令。

new生成

C1在生成LIR时还会遇到很多问题,有些指令,如new、monitor操作,需要与虚拟机的许多组件交互,为它们生成LIR指令是一个复杂且困难的任务,如代码清单8-17所示。

代码清单8-17 new指令LIR生成

void LIRGenerator::do_NewInstance(NewInstance* x) {
CodeEmitInfo* info = state_for(x, x->state());
LIR_Opr reg = result_register_for(x->type()); // 使用rdx存放结果
new_instance(...);
LIR_Opr result = rlock_result(x);
__ move(reg, result); // reg->result
}
void LIRGenerator::new_instance(...) {
// 将klass移动到rdx寄存器
klass2reg_with_patching(klass_reg, klass, info, is_unresolved);
if (UseFastNewInstance && ...) {
... // 特殊处理
} else {
// 生成NewInstanceStub
CodeStub* slow_path = new NewInstanceStub(...);
// 跳转到NewInstanceStub的entry
__ branch(lir_cond_always, T_ILLEGAL, slow_path);
// 从NewInstanceStub跳转回来继续执行__ branch_destination(slow_path->continuation());
}
}

实际上C1并不会为它们生成LIR指令,而是创建一段NewInstanceStub代码,然后跳转到NewInstanceStub的entry执行,如图8-6所示。

NewInstanceStub相当于一个跳床(Trampoline),执行流从外部跳到它的entry,由它调用Runtime1::new_instance分配对象,然后跳到外部的continuation处继续执行。

goto生成

LIRGenerator会为HIR指令goto生成对应的LIR指令,如代码清单8-18所示。

代码清单8-18 do_Goto

void LIRGenerator::do_Goto(Goto* x) {
set_no_result(x);
...
move_to_phi(x->state());
__ jump(x->default_sux());
}

goto的LIR其实就是一个jmp跳转指令。一个值得注意的问题是SSA形式中有Phi指令,而LIR是一种接近物理机器架构的低级中间表示,没有指令集支持Phi,所以必须在生成期间逆变换消除Phi指令。这一步由LIRGenerator::move_to_phi完成,具体思想如图8-7所示。

在HIR中,在不同基本块为同一个变量(假设是x)赋值时可能会使用不同的SSA指令,如图8-7a所示,左边基本块x的赋值被表示为n1=10,右边基本块x的赋值被表示为n2=20,最终它们的后继基本块使用phi指令合并数据,x被表示为n3=[n1,n2],这样符合SSA的定义。但LIR不是SSA,不需要遵守它的规则,且LIR需要更进一步了解底层架构,Phi应当被消除,此时同一个变量x在不同基本块中使用相同的寄存器R1存储。

线性扫描寄存器分配

线性扫描寄存器分配方式会为LIR的虚拟寄存器分配一个物理寄存器,如果物理寄存器的空间不足,则用内存代替(溢出到内存,之前寄存器的读写变成内存地址的读写)。C1使用线性扫描寄存器算法(Linear Scan Register Allocation,LSRA)满足它的设计理念,LSRA算法的具体实现位于c1_LinearScan中,该算法始于do_linear_scan(),如代码清单8-19所示。

代码清单8-19 do_linear_scan()

void LinearScan::do_linear_scan() {
number_instructions();
compute_local_live_sets();
compute_global_live_sets();
build_intervals();
sort_intervals_before_allocation();
allocate_registers();
resolve_data_flow();
if (compilation()->has_exception_handlers()) {
resolve_exception_handlers();
}
propagate_spill_slots();
sort_intervals_after_allocation();eliminate_spill_moves();
assign_reg_num();
...
}

LSRA算法首先通过数据流分析中的经典方式——活跃分析,计算出值的活跃性,以便后续配置构造值的存活范围。

compute_local_live_sets面向单个基本块,它会对基本块中的每条指令进行计算,得到一个live_gen集合和live_kill集合。

live_gen(生成集):在当前基本块中使用,在前驱基本块定义的值。live_gen又称use集。

live_kill(杀死集):在当前基本块定义的值,该值可能“杀死”其在前驱基本块的定义。live_kill又叫def集。

对于每一条LIR指令,这一步都会检查它的输入操作数、输出操作数、临时操作数。如果输入操作数没有位于live_kill集,即当前基本块没有定义,那么将它加入live_gen集。输出操作数和临时操作数都加入live_kill集,因为它们是对值的定义。接着使用compute_global_live_sets将数据流分析扩展至所有基本块,它的核心思想可用下面的数据流方程表示:

该数据流方程的思想源于这些简单的事实:如果在一个基本块内使用了一个值,那么该值一定存在于基本块的live_in集合;如果一个值存在于live_in集合,那么它一定存在于该基本块的某个前驱基本块的live_out集合,因为控制流的边不会生成新的值;如果一个值存在于live_out集合,而且它没有在当前基本块中定义,则当前基本块的live_in集一定包含它。

读者可能已经发现,根据live_gen、live_kill得到基本块live_in和live_out集的过程是一个由下至上的过程,所以活跃分析是一个反向数据流分析。当数据流分析完成后build_intervals开始构造存活范围,如代码清单8-20所示:

代码清单8-20 LIR示例

B2 [6, 14] pred: B3 sux: B3
__id__Operation_____________________________________________
24 label [label:0x31da17c]
26 move [R43|I] [R44|I]
28 mul [R44|I] [R42|I] [R44|I]
30 move [R42|I] [R45|I]
32 sub [R45|I] [int:1|I] [R45|I]
34 move [R44|I] [R43|I]
36 move [R45|I] [R42|I]
38 safepoint [bci:14]
40 branch [AL] [B3]

当build_interval工作时,它会用该基本块的live_out集初始化构造值的存活范围。目前基本块B2的live_out集有R42和R43,初始化它们,如图8-8所示。

图8-8a所示为初始化R42和R43。指令38、40不影响存活范围。指令36会将R42的存活范围修改为[36,42[,同时新增R45的存活范围[24,36[。指令34将R43的存活范围修改为[34,42[,同时新增R44的存活范围[24,36[。当这一步完成后,存活范围如图8-8b所示。随后指令32将R45的存活范围修改为[32,36[。指令30将R45的存活范围修改为[30,36[。指令28将R44的存活范围修改为[28,34[,然后为R42新增存活范围[28,30[。指令26将R44的存活范围修改为[26,34[,为R43新增存活范围[24,26[。当一切完成后,存活范围如图8-8c所示。深色黑条表示该值在该处使用(use_position)。

构造存活范围的核心思想是首先用live_out集初始化存活范围,接着从基本块最后一条指令出发向上遍历,然后根据指令输入、输出临时修改存活范围,具体实现如代码清单8-21所示。

代码清单8-21 build_interval的实现

void LinearScan::build_intervals() {
...
// 反向遍历所有基本块
for (i = block_count() - 1; i >= 0; i--) {...
// 由下至上遍历基本块的所有指令
for (int j = instructions->length() - 1; j >= 1; j--) {
...
// 访问指令的输出操作数
int k, n;
n = visitor.opr_count(LIR_OpVisitState::outputMode);
for (k = 0; k < n; k++) {
LIR_Opr opr=visitor.opr_at(LIR_OpVisitState::outputMode, k);
// 将存活范围的起点修改为当前位置
add_def(opr, op_id, use_kind_of_output_operand(op, opr));
}
// 访问指令的临时操作数
n = visitor.opr_count(LIR_OpVisitState::tempMode);
for (k = 0; k < n; k++) {
LIR_Opr opr = visitor.opr_at(LIR_OpVisitState::tempMode, k);
// 添加新的存活范围为[cur,cur+2]
add_temp(opr, op_id, mustHaveRegister);
}
// 访问指令的输入操作数
n = visitor.opr_count(LIR_OpVisitState::inputMode);
for (k = 0; k < n; k++) {
LIR_Opr opr = visitor.opr_at(LIR_OpVisitState::inputMode, k);
// 添加新的存活范围为[block_start,cur[
add_use(opr, block_from, op_id, use_kind_of_input(op, opr));
}
...}
}
...
}

最后,allocate_register会根据之前得到的存活范围将虚拟寄存器映射到物理寄存器。线性扫描寄存器分配能得到近似图着色寄存器分配的效果且只需线性时间复杂度,这也是C1选择它的主要原因。

本文给大家讲解的内容是深入解析java虚拟机:C1编译器,从HIR到LIR

  1. 下篇文章给大家讲解的是深入解析java虚拟机:C2编译器,编译流程;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

你深入解析过java虚拟机:C1编译器,从HIR到LIR吗?相关推荐

  1. 一文带你学明白java虚拟机:C1编译器,HIR代码优化

    HIR代码优化 为了减少编译时间,C1在抽象解释生成HIR期间,每生成一条SSA指令,都会调用append_with_bci努力尝试若干局部优化.除此之外,HIR构造完成之后,C1还会执行若干轻量级全 ...

  2. Java虚拟机和编译器的区别

    编译器是用来编译java源代码的,以.java为后缀的java源代码必须编译以后才能运行: java虚拟机是java代码的运行环境也就是说被编译器编译以后的java代码在java虚拟机上运行.

  3. java虚拟机编译_[四] java虚拟机JVM编译器编译代码简介 字节码指令实例 代码到底编译成了什么形式...

    前言简介 前文已经对虚拟机进行过了简单的介绍,并且也对class文件结构,以及字节码指令进行了详尽的说明 想要了解JVM的运行机制,以及如何优化你的代码,你还需要了解一下,java编译器到底是如何编译 ...

  4. 一份深入解析Java虚拟机HotSpot手册,让我卷成美团架构师

    前言 Java语言已经走过了20多个年头,在此期间虽然新语言层出不穷,但是都没有撼动Java的位置.可能是历史选择了Java,也可能是Java改变了历史,总之,Java无疑是一门成功的编程语言.这门语 ...

  5. 大多数程序员都懂的java虚拟机:C1编译器从字节码到HIR

    从字节码到HIR 正如之前看到的,C1的HIR是一个基于静态单赋值的图IR,由基本块构成控制流图,由静态单赋值指令构成基本块,如图8-1所示. 所有的指令都派生自Instruction类,其中,Blo ...

  6. Java虚拟机(一)结构原理与运行时数据区域

    前言 本来计划要写Android内存优化的,觉得有必要在此之前介绍一下Java虚拟机的相关知识,Java虚拟机也并不是三言两语能够介绍完的,因此开了Java虚拟机系列,这一篇文章我们来学习Java虚拟 ...

  7. MiniJavaVM——一个Java虚拟机的设计和实现

    http://ba5ag.zrsa.org/paper/ZhuHuaiyi.html MiniJavaVM--一个Java虚拟机的设计和实现 摘要 本文叙述了Java虚拟机(JVM)的概念及如何设计和 ...

  8. 深入理解Java虚拟机-第六章 类文件结构

    第六章 类文件结构 6.1 概述 略 6.2 无关性的基石 因为想要实现 "Write Once,Run Anywhere"的伟大理想,Java 虚拟机被发明了出来.这些虚拟机都可 ...

  9. java 虚拟机 字节码,JAVA虚拟机:虚拟机字节码执行引擎

    "虚拟机"是一个相对"物理机"的概念,这两种机器都有代码执行能力. 物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的. 虚拟机的执行引擎由自己 ...

最新文章

  1. ORM查询语言(OQL)简介--概念篇
  2. Spring5源码 - 00 IOC容器创建_前期准备
  3. 解决php写入mysql乱码问题汇总
  4. Redis 基本数据类型 :String、Hash、List、Set、ZSet
  5. 第5章 Python 数字图像处理(DIP) - 图像复原与重建3 - 爱尔兰(伽马)噪声
  6. 微信小程序中base64转换成图片;uni-app小程序base64转图片;微信小程序base64文件转图片;微信小程序base64图片转图片
  7. [机器学习笔记]Note11--聚类
  8. 当用户流失比较明显后, 如何提升活跃度? push notification 是一个有效的方式吗?...
  9. layui 开启关闭标签_欧盟发布照明产品ErP及能效标签法规新草案
  10. Asp.net1.0 升级 ASP.NET 2.0 的几个问题总结
  11. 谷歌2D景观转3D风景大片,无惧复杂光线与遮挡
  12. Draw Circle 沿着圆运动~
  13. 怎么解决文件正在使用无法删除----资源监视器
  14. 2020-06-20
  15. 注意!某知名国产软件被曝携带木马病毒
  16. myd加入mysql数据库_数据库是.frm,.myd,myi备份如何导入mysql (转)
  17. 【持续更新】【产品相关名词解释】CBD、BOM、DFM、EVT、EOF、CMF、PP、MP等
  18. syswow64删除文件_syswow64,小编告诉你syswow64是什么文件夹
  19. 所有图片类型后缀汇总
  20. uniapp swiper组件被内容撑开

热门文章

  1. DPI 达到丧心病狂的12000!罗技G502告诉你可以的!
  2. Spark 应用开发程序
  3. UE4虚幻引擎UI界面动画制作!
  4. 【Scikit-Learn 中文文档】四十:数据集加载工具 - 用户指南 | ApacheCN
  5. JS 自定义sort方法实现星期的排序【开发记录】
  6. 最易懂得 鸿蒙 实战 - 真机调试 原子服务
  7. 16 个百度网盘搜索引擎
  8. 微信网页开发异常——签名失败
  9. 如何高效管理工作微信
  10. FastCGI 进程管理器(FPM)