Java即时编译器原理解析及实践( 七 )


标量替换
public class Example{@AllArgsConstructorclass Cat{int age;int weight;}public static void example(){Cat cat = new Cat(1,10);addAgeAndWeight(cat.age,Cat.weight);}}经过逃逸分析 , cat对象未逃逸出example()的调用 , 因此可以对聚合量cat进行分解 , 得到两个标量age和weight , 进行标量替换后的伪代码:
public class Example{@AllArgsConstructorclass Cat{int age;int weight;}public static void example(){int age = 1;int weight = 10;addAgeAndWeight(age,weight);}}部分逃逸分析
部分逃逸分析也是Graal对于概率预测的应用 。 通常来说 , 如果发现一个对象逃逸出了方法或者线程 , JVM就不会去进行优化 , 但是Graal编译器依然会去分析当前程序的执行路径 , 它会在逃逸分析基础上收集、判断哪些路径上对象会逃逸 , 哪些不会 。 然后根据这些信息 , 在不会逃逸的路径上进行锁消除、栈上分配这些优化手段 。
4. Loop Transformations
在文章中介绍C2编译器的部分有提及到 , C2编译器在构建Ideal Graph后会进行很多的全局优化 , 其中就包括对循环的转换 , 最重要的两种转换就是循环展开和循环分离 。
循环展开
循环展开是一种循环转换技术 , 它试图以牺牲程序二进制码大小为代价来优化程序的执行速度 , 是一种用空间换时间的优化手段 。
循环展开通过减少或消除控制程序循环的指令 , 来减少计算开销 , 这种开销包括增加指向数组中下一个索引或者指令的指针算数等 。 如果编译器可以提前计算这些索引 , 并且构建到机器代码指令中 , 那么程序运行时就可以不必进行这种计算 。 也就是说有些循环可以写成一些重复独立的代码 。 比如下面这个循环:
循环展开
public void loopRolling(){for(int i = 0;i<200;i++){delete(i);}}上面的代码需要循环删除200次 , 通过循环展开可以得到下面这段代码:
循环展开
public void loopRolling(){for(int i = 0;i<200;i+=5){delete(i);delete(i+1);delete(i+2);delete(i+3);delete(i+4);}}这样展开就可以减少循环的次数 , 每次循环内的计算也可以利用CPU的流水线提升效率 。 当然这只是一个示例 , 实际进行展开时 , JVM会去评估展开带来的收益 , 再决定是否进行展开 。
循环分离
循环分离也是循环转换的一种手段 。 它把循环中一次或多次的特殊迭代分离出来 , 在循环外执行 。 举个例子 , 下面这段代码:
循环分离
int a = 10;for(int i = 0;i<10;i++){b[i] = x[i] + x[a];a = i;}可以看出这段代码除了第一次循环a = 10以外 , 其他的情况a都等于i-1 。 所以可以把特殊情况分离出去 , 变成下面这段代码:
循环分离
b[0] = x[0] + 10;for(int i = 1;i<10;i++){b[i] = x[i] + x[i-1];}这种等效的转换消除了在循环中对a变量的需求 , 从而减少了开销 。
5. 窥孔优化与寄存器分配
前文提到的窥孔优化是优化的最后一步 , 这之后就会程序就会转换成机器码 , 窥孔优化就是将编译器所生成的中间代码(或目标代码)中相邻指令 , 将其中的某些组合替换为效率更高的指令组 , 常见的比如强度削减、常数合并等 , 看下面这个例子就是一个强度削减的例子:
强度削减
y1=x1*3经过强度削减后得到y1=(x1<<1)+x1编译器使用移位和加法削减乘法的强度 , 使用更高效率的指令组 。
寄存器分配也是一种编译的优化手段 , 在C2编译器中普遍的使用 。 它是通过把频繁使用的变量保存在寄存器中 , CPU访问寄存器的速度比内存快得多 , 可以提升程序的运行速度 。
寄存器分配和窥孔优化是程序优化的最后一步 。 经过寄存器分配和窥孔优化之后 , 程序就会被转换成机器码保存在codeCache中 。
四、实践
即时编译器情况复杂 , 同时网络上也很少有实战经验 , 以下是我们团队的一些调整经验 。
1. 编译相关的重要参数

  • -XX:+TieredCompilation:开启分层编译 , JDK8之后默认开启
  • -XX:+CICompilerCount=N:编译线程数 , 设置数量后 , JVM会自动分配线程数 , C1:C2 = 1:2
  • -XX:TierXBackEdgeThreshold:OSR编译的阈值
  • -XX:TierXMinInvocationThreshold:开启分层编译后各层调用的阈值
  • -XX:TierXCompileThreshold:开启分层编译后的编译阈值
  • -XX:ReservedCodeCacheSize:codeCache最大大小
  • -XX:InitialCodeCacheSize:codeCache初始大小
-XX:TierXMinInvocationThreshold是开启分层编译的情况下 , 触发编译的阈值参数 , 当方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数 , 或者当方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数 , 并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时 , 便会触发X层即时编译 。 分层编译开启下会乘以一个系数 , 系数根据当前编译的方法和编译线程数确定 , 降低阈值可以提升编译方法数 , 一些常用但是不能编译的方法可以编译优化提升性能 。