你需要深入解析一下java虚拟机:C2编译器,构造理想图吗?

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

构造理想图

类似于C1从字节码构造HIR,由字节码构造理想图也是一个抽象解释过程。它经过如代码清单9-14的调用链:

代码清单9-14 理想图构造

Parse::Parse()
-> Parse::do_all_blocks()
-> Parse::do_one_block()
-> Parse::do_one_bytecode()

do_one_bytecode()是一个巨大的switch语句,它会对基本块中的每一条字节码进行解释执行。字节码的抽象层次有高有低,不能一概而论。如iload_0加载局部变量到操作数栈,这个字节码不会创造理想图节点,但iadd加法操作会创造理想图节点。do_one_bytecode会将解释状态存放到JVMState中,该对象包含局部变量表和操作数栈等状态。最后JVMState实际位于SafepointNode,因为每一个字节码都是安全点,所以整个理想图构造过程都围绕该节点进行。

构造示例

为了直观展示理想图的构造,考虑如代码清单9-15的Java示例,它包含了源码对应的字节码表示:代码清单9-15 加法操作

public static int add(int val1, int val2){
int sum = val1+ val2 + 12;
return sum;
}
// 对应的字节码
0: iload_0
1: iload_1
2: iadd
3: bipush 12
5: iadd
6: istore_2
7: iload_2
8: ireturn

使用-XX:+PrintIdealGraphLevel=4可以输出理想图构造的详细过程。执行完“iload_0,iload_1”后,理想图的子图如图9-5所示。

图9-5 “iload_0,iload_1”对应的理想图

Parm#10和Parm#11分别表示add的参数val1和val2。iload_0和iload_1不创建新节点,它们只是将Parm#10和Parm#11设置为SafePoint#22的输入。执行完iadd后的理想图如9-6所示。

图9-6 iadd对应的理想图

iadd创建了AddINode节点,它将Parm#10和Parm#11作为输入,相加后得到结果并输出到SafePoint#22。SafePoint#22的JVMState包含操作数栈,只有它能存储状态。下一步bipush字节码执行完的理想图如图9-7所示。

图9-7 bipush 12对应的理想图

bipush创建了ConI#24,表示常量12,同样设置为SafePoint#22的输入。第二个iadd执行完的理想图如图9-8所示。

图9-8 iadd对应的理想图

第一个加法AddI#23作为一个输入,表示常量12的节点ConI#24作为第二个输入,二者均流入第二个加法AddI#25,并将结果输出到SafePoint#22。接下来执行istore_2,iload_2,不创建节点,只是修改SafePoint#22的状态。当遇到ireturn字节码时,理想图会调整边,如图9-9所示。

图9-9 ireturn对应的理想图

Parm#5表示control值,即控制流输入,Region#14节点接收多个control输入,然后选择一个作为输出,此处Region#14只有Parm#5一个control输入,配合只有一个输入的Phi#18,最终输出到SafePoint#13。C2会调用
GraphKit::stop_and_kill_map将之前的SafePoint#22标记为死节点,并不再使用。do_one_bytecode()遇到ireturn时只是设置控制流走向,ReturnNode的创建实际是在解析完毕后的Compile::return_values中实现,解析阶段最终生成的理想图如图9-10所示。

图9-10 最终理想图

上述过程遗漏了一个重要内容,每当解析阶段向理想图插入新生成的节点时,它会对新插入的节点调用PhaseGVN::transform,该函数会应用一系列优化技术,包括Identity、Ideal和GVN,使得每个节点最优,继而使理想图局部最优。

Identity、Ideal、GVN

理想图的节点由opto/node的Node类表示,Node类有三个虚函数,如代码清单9-16所示:

代码清单9-16 C2 Node

class Node{
protected:
Node **_in; // 输入边数组
Node **_out; // 输出边数组
node_idx_t _cnt; // 要求的输入边大小
node_idx_t _max; // 真实的输入变大小
node_idx_t _outcnt; // 输出边大小
node_idx_t _outmax; // 真实的输出边大小
public:
virtual Node* Identity(PhaseGVN* phase);
virtual Node *Ideal(PhaseGVN *phase, bool can_reshape);
...
};

Node的两个虚函数分别对应Identity和Ideal优化。Identity寻找与当前节点计算结果相同的其他节点,并用其代替当前节点,与GVN不同的是,替换当前节点的其他节点可以是不同类型的。AddNode的Identity如代码清单9-17所示,它清晰地阐述了Identity的概念:

代码清单9-17 AddNode::Identity

Node* AddNode::Identity(PhaseGVN* phase) {
const Type *zero = add_id();
if(phase->type(in(1))->higher_equal(zero)) return in(2);
if(phase->type(in(2))->higher_equal(zero)) return in(1);
return this;}

AddNode会判断两个输入边是否存在一个0,如果存在则使用另一个输入代替当前节点。也就是说,当前节点的行为是计算x+0,它会检查第一条输入x和第二条输入0,如果发现第二条输入是0,则使用第一条输入x代替x+0这个节点。类似的,MulNode的Identity如代码清单9-18所示:

代码清单9-18 MulNode::Identity

Node* MulNode::Identity(PhaseGVN* phase) {
const Type *one = mul_id(); // The multiplicative identity
if(phase->type(in(1))->higher_equal(one)) return in(2);
if(phase->type(in(2))->higher_equal(one)) return in(1);
return this;
}

如果乘法运算中的乘数或者被乘数为1,那么使用上一次的计算结果(输入变量)作为当前节点的值,而不再计算当前节点,所以MulNode::Identity会将1*x优化为x。

Ideal表示理想化,它会返回一个比当前节点更“理想”的节点。关于理想化,目前还没有一个统一的定义,不过大致可以认为是方便C2后续优化的一种形式。代码清单9-19显示了AddNode的Ideal实现:

代码清单9-19 AddNode::Ideal

Node *AddNode::Ideal(PhaseGVN *phase, bool can_reshape) {
...
// 将(x+1)+2转换为x+(1+2)
Node *add1 = in(1);
Node *add2 = in(2);
if( con_right && t2 != Type::TOP && // 右边输入是常量?
add1_op == this_op ) { // 左边输入是加法?
const Type *t12 = phase->type( add1->in(2) ); // x+1的1
if(t12->singleton() && t12 != Type::TOP) { // 左边加法是变量+常量?
Node *x1 = add1->in(1);
Node *x2 = phase->makecon( add1->as_Add()->add_ring( t2, t12 ));
...
set_req(2,x2); // 当前节点第二个输入设置为3
set_req(1,x1); // 当前节点第一个输入设置为x
}
}
// 将(x+1)+y转换为(x+y)+1,将x+(y+1)转换为(x+y)+1,代码与上面类似
...
return progress;
}

上面注释提到的将(x+1)+2转换为x+(1+2)只是转化的思路,实际上Ideal是将(x+1)+2转换为x+3,这样使得树状结构层次下降,扁平化了表达式树,同时也做了一个简单的常量折叠优化。

Ideal也具有规范化的意义,它将常量都放到第二个输入中,相当于选择了一种加法表达式的统一形式,以便后续优化。

Identity和Ideal优化均属于最小的局部优化,如果子类节点有需要可以重写它们。

GVN是一个全局的优化,它需要除节点本身外其他所有节点的信息,所以不能简单表示为Node类的成员函数。GVN类似于Identity,它的哈希表存放了当前存在的所有节点,当插入新节点时,GVN会从哈希表中寻找一个与当前插入节点等价的节点,如果存在,就用找到的节点代替当前插入的节点。与当前插入节点等价意味着节点本身类型以及它们的输入都是相同的,在这种情况下,无须插入新节点,使用等价节点代替即可。

假设处理的代码是(x+1)*(x+1),处理完乘法运算左边后,哈希表存在一个AddINode表示x+1,当处理右边操作数时,由于哈希表已经存在等价节点,直接使用它代替即可,无须插入新节点。MulINode发现两条输入边是相同元素后,可以应用Ideal优化技术将它优化为(x+1)^2形式。

本文给大家讲解的内容是深入解析java虚拟机:C2编译器,构造理想图

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值