zl程序教程

您现在的位置是:首页 >  后端

当前栏目

秒懂深入解析java虚拟机:C2编译器,机器无关优化有多牛?

JAVA机器虚拟机编译器 优化 深入 解析 无关
2023-06-13 09:14:12 时间

机器无关优化

IGVN

C2的PhaseIterGVN实现了IGVN,它是一个典型的不动点算法。

IGVN每次从工作集获取一个节点,如果节点没有输出边,那么该节点是个死节点,可以安全移除。C2会递归式地移除死节点的输入边,这一步又可能产生新的死节点。如果节点有输出边,对该节点应用transform_old进行变形(transform_old调用节点的Ideal、Identity和GVN优化),如果节点变换成功,会将新节点加入工作集。如此反复,直到工作集没有节点,即没有节点可以再次优化。具体优化过程如代码清单9-20所示:

代码清单9-20 IGVN

void PhaseIterGVN::optimize() {
uint loop_count = 0;
// 从worklist获取节点,然后对节点变形。如果发生变化,则将节点再次加入worklist。
while(_worklist.size()) {
// 从worklist中获取一个元素
Node* n = _worklist.pop();
...// 特殊情况,这一步的迭代次数超过C2限制
// 如果节点输出边不为空,则进行转换,否则直接移除
if (n->outcnt() != 0) {
// Do the transformationNode* nn = transform_old(n);
} else if (!n->is_top()) {
remove_dead_node(n);
}
}
}

由于其他优化对理想图变换后可能产生新的优化机会,且IGVN是个轻量级优化,所以C2会在很多其他优化的后面插入IGVN(通常形式是igvn.optimize())使优化效果达到最佳。

逃逸分析

Compile::Optimize阶段会调用 ConnectionGraph::do_analysis进行逃逸分析。逃逸分析通过建立连接图(Connection Graph,CG)分析对象和对象引用的关系,可以知道对象是否逃逸出方法(即对象是否是该方法的局部变量)以及对象是否逃逸出创建该对象的线程(即其他线程能否访问该对象)。如果对象没有逃逸出方法,则可以在栈上分配而无须在堆上分配。栈分配的性能比堆分配更佳,同时栈上分配减少了对象访问的开销和垃圾回收器的负载。如果对象没有逃逸出线程,那么可以消除对象上可能存在的同步对象锁;如果线程与处理器亲和性较强,可以将对象分配在线程关联的处理器的多级缓存上,提高数据局部性。

逃逸分析的核心是连接图。连接图的节点有对象、对象引用和对象字段三种,边包括表示对象引用A指向对象B的指向边(P)、表示对象引用指向对象引用的Deferred边(D)以及表示对象指向对象字段的字段边(F)。之所以只包含这些元素是因为逃逸分析关注的目标只有对象赋值(T t = new T),引用赋值(T a = t),字段赋值(g.f = a,a=g.h)四种,而这四种刚好可以通过三种节点和四种边的图结构建模,如图9-11所示。

图9-11 连接图

逃逸分析引入了三种类型格(Type Lattice):NoEscape (T)、ArgEscape和GlobalEscape (⊥)。NoEscape表示没有逃逸出创建它的方法,ArgEscape表示对象作为方法实参逃逸出创建它的方法,GlobalEscape表示对象完全逃逸出方法和线程。逃逸分析的示例如代码清单9-21所示:

代码清单9-21 逃逸分析Java代码示例

class ListElement {
int data;
ListElement next;
static ListElement g = null;
ListElement() {data = 0; next = null;}
static void L(int p, int q){S0: ListElement u = new ListElement();
ListElement t = u;
while(p > q){
S1: t.next = new ListElement();
t.data = q++;
t = t.next;
}
S2: ListElement v = new ListElement();
NewListElement.T(u, v);
}
}
class NewListElement{
ListElement org;
NewListElement next;
NewListElement() {org = null; next = null;}
static void T(ListElement f1, ListElement f2){
S3: NewListElement r = new NewListElement();
while(f1 != null){
S4: r.org = f1.next;
S5: r.next = new NewListElement();
. . . // 做一些计算之类
S6: r = r.next;
if(f1.data == 0){
S7: ListElement.g = f2;
}
f1 = f1.next;}
}
}

对应的连接图如图9-12所示,连接图中的虚线表示D边,实线表示P边或者F边,圆圈表示引用,方框表示对象实体。整个图的最外部虚线方框表示在分析过程中我们关心的四个程序点:调用方法L()前,方法L()入口,方法L()返回,调用方法L()后。虚线圆圈表示每个程序点的连接图状态。

图9-12 逃逸分析Java示例对应的连接图

逃逸分析在调用NewListElement.T()前建立连接图,然后进入方法入口。为了便于分析,这里创建了虚引用节点(Phantom ReferenceNode)a1和a2。接着在方法返回时用连接图为程序建模,分割出三种逃逸状态的子图:ArgEscape子图、GlobalEscape子图(二者取并集又叫NonLocalGraph),以及NoEscape子图(LocalGraph)。简单观察可知,方法退出的连接图中只存在LocalGraph指向NonLocalGraph的边,而没有反向的边,所以LocalGraph包含的S3和S5语句理论上是可以在栈上分配的。

NonLocalGraph则作为方法L的逃逸分析结果,供后续对相同方法调用时直接使用,无须再做分析。不过调用者(方法L)不能直接使用被调用者(方法T)的逃逸分析结果,需要经过一个映射过程,即将被调用者的分析结果中的节点和边映射到调用者的连接图上,如将ArgEscape的a1映射到图9-12f的a1。将ArgEscape的a1指向的s4映射到图9-12f的s0,将ArgEscape的s4映射到图9-12f的s1,由于s4的next指向本身,所以同时为s1和s0的next插入指向本身和对立的边。

向量化

为了支持计算密集的多媒体应用程序,现代处理器在其各自指令集架构中新增了很多SIMD指令。SIMD表示单指令多数据(SingleInstruction Multiple Data),它是指将多个数据“打包”到单个专门的寄存器,然后用一条指令完成计算,如图9-13所示。

图9-13 SIMD示例

使用一条SIMD完成了四个整数的加法运算。不同处理器的SIMD具体指令集实现各有不同,如ARM是Neon。x86最初的SIMD实现是SSE指令集,如图9-14所示。

图9-14 x86的SSE/AVX寄存器

SSE包含xmm0~15,每个xmm寄存器可以存放128位数据。2011年发布的AVX指令集扩展了SSE指令集,支持256位的ymm0~15寄存器。

2015年的AVX512又扩展了AVX指令集,支持zmm0~31寄存器,且单个寄存器达到了惊人的512位。

由于免费的硬件性能“午餐”已经结束,人们自然注意到了SIMD。

C2的opto/superword提供了自动向量化优化,可以将满足条件的代码优化为使用SIMD指令操作。自动向量化是C2系列循环优化之一,是PhaseIdealLoop的子过程,由SuperWord::transform_loop完成。

transform_loop对于哪些代码能进行循环向量化有严格要求。简单来说,只对循环展开后的代码进行向量化,而只有计数循环(Counted Loop)能循环展开,所以只有循环展开的计数循环能向量化。所谓计数循环是指步长是常量,终止条件是循环不变量,且只有一条退出路径的循环,如代码清单9-22所示:

代码清单9-22 计数循环

public static void vecSum(int[] a, int[] b, int[] c){
for(int i = 0; i < 25; i++){
c[u] = a[i] + b[i];
}
}

循环终止条件25是循环不变量(在循环期间不会改变的值,也可以不是常量),步长是常量1,最终产出的部分代码如代码清单9-23所示:

代码清单9-23 向量化

...
B12:
vmovdqu XMM0,[RDX + #16 + R9 << #2] !xmm0=b[i:i+3]
vpaddd XMM0,XMM0,[RBX + #16 + R9 << #2] !xmm0+=a[i:i+3]
vmovdqu [RDI + #16 + R9 << #2],XMM0 !c[i:i+3]=xmm0
vmovdqu XMM0,[RDX + #48 + R9 << #2] !xmm0=b[i+4:i+7]
vpaddd XMM0,XMM0,[RBX + #48 + R9 << #2] !xmm0+=a[i+4:i+7]
vmovdqu [RDI + #48 + R9 << #2],XMM0 !c[i+4:i+7]=xmm0
addl R9, #16
cmpl R9, #18
jge,s B18...

实际上向量化将循环分为pre-loop、main-loop、post-loop三个阶段,将单个循环展开成三个循环阶段,代码清单9-36展示的是main-loop,它两次使用vpadd指令,相当于一次对16个元素相加。

本文给大家讲解的内容是深入解析java虚拟机:C2编译器,机器无关优化

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

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。