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

1496 篇文章 10 订阅
1494 篇文章 14 订阅

HIR代码优化

为了减少编译时间,C1在抽象解释生成HIR期间,每生成一条SSA指令,都会调用append_with_bci努力尝试若干局部优化。除此之外,HIR构造完成之后,C1还会执行若干轻量级全局优化。本节将详细描述这些优化的执行过程。这些优化都位于build_hir()。

规范化

C1解释执行基本块字节码构造SSA指令时会进行规范化(Canonicalize[1]),将HIR指令转化为一种更简洁、更统一的形式,具体说明如下。

算术运算:如果整数减法的两个操作数相同则用常量0代替。如果加、减、乘、除、求余、位与、位或、位异或的两个操作数都是常量,则编译器用常量代替计算指令。

ArrayLength:JVM的arraylength字节码可以取数组长度。在规范化期间如果发现数组是编译器可知的字面值,则用常量代替这条指令。

比较运算:如果比较运算的两个操作数都是相同的值,则用常量0代替。

Intrinsic:如果是一些@HotSpotIntrinsicCandidate标注的函数,比如java.lang.Float的floatToIntBits(),C1将计算出常量结果,然后用该常量代替函数调用。

C1的规范化实现于c1_Canonicalizer。每当将一条字节码转换为一条SSA指令时,调用append_with_bci的过程中就会应用规范化,规范化是这些局部优化执行的最佳时机。代码清单8-10所示以NegateOp为例展示了规范化的具体实现。

代码清单8-10 NegateOp规范化

void Canonicalizer::do_NegateOp(NegateOp* x) {
ValueType* t = x->x()->type();
if (t->is_constant()) { // 如果-x中x为常量,那么使用常量代替
switch (t->tag()) {
case intTag:
set_constant(-t->as_IntConstant ()->value()); return;
...// long、float、double同样
default : ShouldNotReachHere();
}
}
}

当新插入NegateOp时,C1会检查NegateOp的操作数是否为常量,即是否为诸如-3、-4.3这样的常量,如果是常量那么可以不插入NegateOp,而是使用常量代替。规范化涉及的优化/变形是简单但确有成效的,了解它们是了解编译器优化的一个良好开端。

内联

方法调用是一个开销昂贵的操作,它可以将参数从一个栈帧传递到另一个栈帧,也可以保留栈空间、设置EIP指针等。对于一些简单方法,如getter、setter,通过内联可以减少它们的调用开销。更重要的是,内联可以将复杂且耗时的跨过程分析/优化转换成更简单的过程内分析/优化,所以更多的内联可以触发后续更多的优化。

当C1解释执行基本块的字节码构造SSA指令时,如果遇到4条invoke字节码,它会调用GraphBuilder::try_inline()尝试内联。C1目前默认内联不超过35字节的方法,可以通过-XX:MaxInlineSize=val修改该限制。

对于静态方法,内联是比较简单的,但是虚方法的内联相对困难,因为具体的调用者类型是动态的。如果调用某个方法取决于它的调用者的类型,那么该方法被称为多态方法。如果调用者在运行时总是被派发到相同类的虚方法,那么该方法被称为单态(Monomorphic)方法。

Java方法虽然默认都是虚方法,但是在实际使用中大多数调用都是单态调用。为了识别单态方法,C1在调用try_inline()前会执行类层次分析(Class Hierarchy Analysis,CHA),在找到单态方法后再尝试内联。

随之而来的问题是,CHA是对当时虚拟机加载类的依赖图进行分析得到一个方法,该依赖图并不是永久成立的,如图8-3所示。

如图8-3所示,假设类B没有加载进虚拟机,编译器乐观地假设只存在A,并找到只有A.bar()符合要求然后进行内联。后面某个时候如果create()加载了类B,破坏了之前CHA分析的依赖图,此时虚拟机必须准备逃生窗口,停止编译后,跳转到未编译的代码继续执行,并使用退优化回退到解释器解释执行代码的阶段,这个过程类似于栈上替换的逆操作。退优化还需要处理从编译后的代码到解释器之间栈布局的不同而带来的问题。

 基本块优化

使用-XX:+UseC1Optimizations可以开启基本块优化,基本块优化包括条件表达式消除和空检查消除。

条件表达式消除(Conditional Expression Elimination)会检查CFG中的条件表达式,然后使用IfOp指令替换条件表达式。这样可以生成更高效的机器代码,因为有些后端指令集包含条件传送指令(cmovecc,setcc),可以直接实现IfOp指令。Java是一门安全的语言,当访问对象为NULL时必须抛出对应的空指针异常。在每次访问对象前,虚拟机必须检查对象是否为NULL。

空检查消除优化(Null Check Elimination)会尝试消除一些显式的空检查,或者将它们替换为隐式检查。如果可以证明对象不为NULL,比如同时访问对象两次,第一次已经检查过,那么第二次检查就可以消除。

值编号

C1值编号的实现位于c1_ValueMap.hpp中。每个基本块对应一个ValueMap,由于支持全局值编号,为了避免后继基本块复制当前基本块的内容,ValueMap被组织成一个具有层级的哈希表,使用一个_nesting字段表示层级。

C1同时包含局部值编号和全局值编号。局部值编号发生在C1解释执行基本块的字节码构造的SSA指令中,如代码清单8-11所示。

代码清单8-11 局部值编号

Instruction* GraphBuilder::append_with_bci(...) {
...
if (UseLocalValueNumbering) {
// 寻找当前基本块的值编号表中是否存在值i1
Instruction* i2 = vmap()->find_insert(i1);
if (i2 != i1) {
// 如果值编号表中存在i1,则复用它
return i2;
}
// kill集计算
ValueNumberingEffects vne(vmap());
i1->visit(&vne);
}
...
}

局部值编号可简单地看作哈希表查重的过程。但是实际情况要复杂一些,正如之前提到的,假设存在v1、v2都是读取同一个数组相同索引的元素,即便它们的值编号相同,也不能用v1代替数组元素读取操作,因为在v1、v2读取中可能存在对数组相同位置赋值的操作,如图8-4所示。

当v1和v2间发生赋值,就可认为赋值操作“杀死”了前面已读取的值。除了赋值操作外,monitor指令和方法调用也会“杀死”前面所有内存读取操作,因为调用的方法可能对内存做任何事情。代码清单8-11中的ValueNumberingEffects就是用来计算这些可能“杀死”读操作的方法的。

全局值编号发生于HIR构造完毕后,与局部值编号的代码类似,只是涉及多个基本块,需要考虑kill集的传递和Phi节点的问题。

数组范围检查

根据Java的语义规范,在访问数组时,虚拟机需要检查索引是否是一个有效值,并在索引无效的情况下抛出
ArrayIndexOutOfBoundsException异常。对于一些计算密集或数学应用程序,频繁地进行数组访问索引检查是会产生不小的开销,数组范围检查消除(Range Check Elimination)旨在对程序进行静态分析,以此消除一些不需要的数组范围检查操作,如代码清单8-12所示。

代码清单8-12 安全数组访问

public static void zero(int[] arr){
for(int i=0;i<arr.length;i++){
arr[i] = 0;
}
}

在证明了i总是位于有效数组范围后,可以完全消除循环中数组赋值前的检查。

 循环不变代码外提

如果关闭分层编译,执行GVN优化前会使用ShortLoopOptimizer做一些简单的循环优化,如循环不变代码外提(Loop Invariant CodeMotion,LCM)。LCM是指将循环中不变的值移动到循环外面,以消除每次都要进行的计算,如代码清单8-13所示。

代码清单8-13 循环不变代码外提

void LoopInvariantCodeMotion::process_block(BlockBegin* block) {
...
// 形参表示位于循环的所有基本块。遍历基本块中的每一条指令
while (cur != NULL) {
bool cur_invariant = false;// 如果指令是常量且不能发生trap;或者指令是算术/逻辑/位运算,指令读取字段值
// 等;再或者指令获取数组长度,且数组长度是不变代码。那么该指令是循环不变代码
if (cur->as_Constant() != NULL) {
cur_invariant = !cur->can_trap();
} else if (cur->as_ArithmeticOp() != NULL || cur->as_LogicOp() !=
NULL || cur->as_ShiftOp() != NULL) {
Op2* op2 = (Op2*)cur;
cur_invariant = ...;
} else if (cur->as_LoadField() != NULL) {
cur_invariant = ...;
} else if (cur->as_ArrayLength() != NULL) {
ArrayLength *length = cur->as_ArrayLength();
cur_invariant = is_invariant(length->array());
} else if (cur->as_LoadIndexed() != NULL) {
LoadIndexed *li = (LoadIndexed *)cur->as_LoadIndexed();
cur_invariant = ...;
}
// 如果该指令是循环不变代码
if (cur_invariant) {
// 将该指令从循环内部移动到循环前面
Instruction* next = cur->next();
Instruction* in = _insertion_point->next();
_insertion_point = _insertion_point->set_next(cur);
cur->set_next(in);
...
} else {
prev = cur;cur = cur->next();
}
}
}

LCM遍历构成循环的所有基本块,然后遍历基本块的每一条指令,当发现满足要求的循环不变代码时,将循环不变代码从循环基本块中移除,然后添加到insertion_point所在的基本块,insertion_point即支配循环头的基本块,具体示例如代码清单8-14所示。

代码清单8-14 循环不变代码外提Java代码示例

public class LoopInvariantMotion {
private static int[] arr = new int[]{1,2,3,4};
public static void loopInvariant(){
int s = 0;
for(int i=0;i<10;i++){
s += arr.length; // 循环不变代码arr.length
s += arr[2]; // 循环不变代码arr[2]
}
}
}

对应的HIR如图8-5左侧所示,其中B1和B2构成for循环,B0基本块支配B1基本块。

当发现循环基本块B2中的两个不变量后,C1会将它移到循环外面的B0基本块中,B0基本块支配循环头基本块B1。

本文给大家讲解的内容是深入解析java虚拟机:C1编译器,HIR代码优化

  1. 下篇文章给大家讲解的是深入解析java虚拟机:C1编译器,从HIR到LIR;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值