vlambda博客
学习文章列表

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

从字节码到HIR

正如之前看到的,C1的HIR是一个基于静态单赋值的图IR,由基本块构成控制流图,由静态单赋值指令构成基本块,如图8-1所示。


所有的指令都派生自Instruction类,其中,BlockBegin表示基本块的起点,BlockEnd表示基本块的结束。BlockBegin和BlockEnd合起来表示一个基本块,BlockBegin的predecessors表示当前基本块的前驱块,BlockEnd的successors表示当前基本块的后继块,它们连接起来组成一幅控制流图。BlockBegin的next指向基本块的下一条指令,如LogicOp、LoadField等;下一条指令的next又指向再下一条,如此反复,最终形成一个指令序列,即基本块内部的SSA指令链表。

build_hir()会创建一个GraphBuilder对象,而这个创建的过程就是字节码转换为HIR的过程。该过程主要分为两步:首先使用BlockListBuilder划分出所有基本块,找出循环头,然后使用SSA指令(即Instruction的子类)填充每个基本块。

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


识别基本块

字节码是线性结构,所以在识别时可以使用BlockListBuilder线性地遍历字节码,找到if_cmp*、goto、throw、return、tableswitch、lookupswitch这些可以改变控制流的字节码,将它们标记为leader字节码,并据此划分出基本块的边界,如代码清单8-8所示。

代码清单8-8 划分基本块,找出循环头

BlockListBuilder::BlockListBuilder(...){
set_entries(osr_bci); // 设置入口基本块,会特殊处理OSR入口点
set_leaders(); // 找到leader字节码,划分基本块
mark_loops(); // 标记循环
}BlockListBuilder

还会标记循环,这个过程也是必要的,因为循环头所在的基本块可能存在多个前驱基本块,而多个前驱基本块隐含着一个变量可能会有不同的定义,所以为了合并同一个变量可能存在的不同定义,编译器需要创建Phi节点。编译器也包含了许多与循环相关的优化,它们都要求知晓循环所在。

抽象解释

当划分出基本块和找出循环头后,控制流图(CFG)已经初具雏形,但是基本块内部还是空的,换句话说,BlockBegin的next是空的,需要使用Instruction填充基本块。填充代码如代码清单8-9所示。

代码清单8-9 填充基本块

GraphBuilder::GraphBuilder(...){
// 划分基本块,找出循环头
BlockListBuilder blm(compilation, scope, osr_bci);
...// 设置控制流图入口基本块状态
_initial_state = state_at_entry();
start_block->merge(_initial_state);
switch (scope->method()->intrinsic_id()) {
... // 特殊处理一些intrinsic方法
default:
scope_data()->add_to_work_list(start_block);// 对于每个基本块,遍历字节码,解释得到SSA指令并填充基本块
iterate_all_blocks();
break;
}
...
}

由于CFG是图结构,C1将使用广度优先遍历,而广度优先遍历的实现通常需要用一个队列进行辅助,该队列即代码清单8-9所示的worklist。iterate_all_blocks()将使用广度优先遍历对每个基本块进行遍历,并对每个基本块的字节码抽象解释(Abstract Interpretation)。

所谓抽象解释是指C1像模板解释器一样,解释执行基本块对应的字节码,并生成对应的SSA指令。解释过程中需要的局部变量和操作数会放到ValueStack,如图8-2所示。


以图8-2所示为例,假设图中所示是一个基本块,包含了左边的字节码。C1解释执行字节码,并将状态放到ValueStack中。状态包括存放局部变量与函数入参的local和存放临时计算结果的stack。左侧的[i7,i8]表示局部变量,当解释iload_1时,加载局部变量i8到ValueStack中,该字节码不生成SSA指令;当解释iload_0时,加载i7到ValueStack,该字节码不生成SSA指令;当解释imul时,该字节码会生成SSA指令,该指令以ValueStack的两个值作为参数,产出新的值i11并放入ValueStack;当解释istore_1时,将i11放入局部变量表的第二个槽,该字节码不产生SSA指令。

解释完成后生成的三条SSA指令会填充到基本块中,至此HIR的构造就完成了,之前基于栈的字节码变成了基于寄存器的SSA指令。如果读者构造HotSpot VM时使用的是fastdebug类型,加上-XX:+PrintIR参数可以输出每一个步骤的HIR(这一步对应输出的IR after parsing阶段)。

注意,C1生成SSA指令后并非简单地加入基本块,而是会调用append_with_bci函数,该函数会对当前生成的SSA指令进行若干局部优化,如常量折叠、局部值编号等。换句话说,这些(由于SSA本身的特性决定)轻量级的优化在HIR构造完成时就已经完成了,而build_hir()实现的一些HIR优化是更为复杂,也相对重量级的优化。上面提到的这些轻量级优化的内容将在下节描述。


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

  1. 下篇文章给大家讲解的是深入解析java虚拟机:C1编译器,HIR代码优化;

  2. 觉得文章不错的朋友可以转发此文关注小编;

  3. 感谢大家的支持!