int foo(int x, int y, int[] a) { int sum = 0; for (int i = 0; i < a.length; i++) { sum += x * y + a[i]; } return sum; }
// 对应的字节码 int foo(int, int, int[]); Code: 0: iconst_0 1: istore 4 3: iconst_0 4: istore 5 6: goto 25 // 循环体开始 9: iload 4 // load sum 11: iload_1 // load x 12: iload_2 // load y 13: imul // x*y 14: aload_3 // load a 15: iload 5 // load i 17: iaload // a[i] 18: iadd // x*y + a[i] 19: iadd // sum + (x*y + a[i]) 20: istore 4 // sum = sum + (x*y + a[i]) 22: iinc 5, 1 // i++ 25: iload 5 // load i 27: aload_3 // load a 28: arraylength // a.length 29: if_icmplt 9 // i < a.length // 循环体结束 32: iload 4 34: ireturn
x*y
和循环判断条件中的 a.length
均属于循环不变代码 x*y
是整数乘法运算, a.length
是内存访问操作,读取数组对象a的长度 arraylength
指令来访问 理想情况下,经过循环无关代码外提后,等同于下面的手工优化版本
int fooManualOpt(int x, int y, int[] a) { int sum = 0; int t0 = x * y; int t1 = a.length; for (int i = 0; i < t1; i++) { sum += t0 + a[i]; } return sum; }
即时编译器除了将 x*y
和 a.length
外提,还提供int数组加载指令 iaload
所暗含的 null check
和 range check
,伪代码如下
int iaload(int[] arrayRef, int index) { if (arrayRef == null) { // null check throw new NullPointerException(); } if (index < 0 || index >= arrayRef.length) { // range check throw new ArrayIndexOutOfBoundsException(); } return arrayRef[index]; }
foo方法中的 null check
属于 循环无关
代码,即与第几次循环无关,将iaload伪代码展开,得到
int foo(int[] a) { int sum = 0; for (int i = 0; i < a.length; i++) { if (a == null) { // null check throw new NullPointerException(); } if (i < 0 || i >= a.length) { // range check throw new ArrayIndexOutOfBoundsException(); } sum += a[i]; } return sum; }
在C2中, null check
的外提是通过 额外的编译优化
(
即 循环预测
,-XX:+UseLoopPredicate)来实现的
该优化的实际做法就是在 循环之前 插入 同样 的检测代码,并在命中的时候进行 去优化
int foo(int[] a) { int sum = 0; if (a == null) { deoptimize(); // never returns } for (int i = 0; i < a.length; i++) { if (a == null) { // now evluate to false throw new NullPointerException(); } if (i < 0 || i >= a.length) { // range check throw new ArrayIndexOutOfBoundsException(); } sum += a[i]; } return sum; }
由于如果外提 range check
之后,将无法再引用到循环变量,因此即时编译器需要 转换检测条件
for (int i = INIT; i < LIMIT; i += STRIDE) { if (i < 0 || i >= a.length) { // range check throw new ArrayIndexOutOfBoundsException(); } sum += a[i]; } ---------- // 经过range check外提之后 if (INIT < 0 || IMAX >= a.length) { // IMAX是i所能达到的最大值,不一定是LIMIT-1 detopimize(); // never returns } for (int i = INIT; i < LIMIT; i += STRIDE) { sum += a[i]; // 不再包含range check }
循环展开: 在循环体中重复多次循环迭代 ,并 减少循环次数 的编译优化,是一种以 空间换时间 的优化方式
int foo(int[] a) { int sum = 0; for (int i = 0; i < 64; i++) { sum += (i % 2 == 0) ? a[i] : -a[i]; } return sum; }
经过一次 循环展开 后
int foo(int[] a) { int sum = 0; for (int i = 0; i < 64; i += 2) { // 步长为2 sum += (i % 2 == 0) ? a[i] : -a[i]; sum += ((i + 1) % 2 == 0) ? a[i + 1] : -a[i + 1]; } return sum; }
在 C2 中,只有 计数循环 才能被展开,计数循环需要满足以下条件
for (int i = START; i < LIMIT; i += STRIDE) { .. } // 等价于 int i = START; while (i < LIMIT) { .. i += STRIDE; } // 只要LIMIT是与循环无关的数值,STRIDE是常数,而且循环中除了i < LIMIT之外没有其他基于循环变量i的循环出口 // 那么C2便会将该循环标识为计数循环
循环展开的缺点: 增加了代码的冗余度 ,导致所 生成机器码的长度大幅上涨
但随着循环体的增大,优化机会也会不断地增加,一旦循环展开能触发 进一步的优化 ,总体的代码复杂度也将降低
int foo(int[] a) { int sum = 0; for (int i = 0; i < 64; i += 2) { sum += a[i]; sum += -a[i + 1]; } return sum; }
完全展开:当循环的数量是 固定 值而且 非常小 ,即时编译器将循环完全展开
原本循环中的循环判断语句将不复存在,变成了若干 顺序执行 的循环体
int foo(int[] a) { int sum = 0; for (int i = 0; i < 4; i++) { sum += a[i]; } return sum; }
完全展开
int foo(int[] a) { int sum = 0; sum += a[0]; sum += a[1]; sum += a[2]; sum += a[3]; return sum; }
即时编译器会在 循环体的大小 与 循环展开次数 之间作出 权衡
循环判断外提: 将循环中的if语句外提到循环之前,并且在该if语句的两个分支中分别放置一份循环代码
int foo(int[] a) { int sum = 0; for (int i = 0; i < a.length; i++) { if (a.length > 4) { sum += a[i]; } } return sum; }
// 循环判断外提 int foo(int[] a) { int sum = 0; if (a.length > 4) { for (int i = 0; i < a.length; i++) { sum += a[i]; } } else { for (int i = 0; i < a.length; i++) { } } return sum; } // 进一步优化 int foo(int[] a) { int sum = 0; if (a.length > 4) { for (int i = 0; i < a.length; i++) { sum += a[i]; } } return sum; }
循环 判断外提 与循环 无关检测外提 所针对的代码模式比较类似,都是循环中的 if语句
循环 无关检测外提 在检查失败时会 抛出异常 , 中止当前的正常执行路径
循环 判断外提 针对更 常见的情况 ,即通过if语句的不同分支执行不同的代码逻辑
循环剥离:将循环的 前几个迭代 或者 后几个迭代 剥离出循环的优化方式
通过将这几个特殊的迭代剥离出去,使得原本的循环体的 规律性更加明显 ,从而 触发进一步优化
int foo(int[] a) { int j = 0; int sum = 0; for (int i = 0; i < a.length; i++) { sum += a[j]; j = i; } return sum; }
剥离第一个迭代
int foo(int[] a) { int sum = 0; if (0 < a.length) { sum += a[0]; for (int i = 1; i < a.length; i++) { sum += a[i - 1]; } } return sum; }
转载请注明出处:http://zhongmingmao.me/2019/01/06/jvm-advanced-optimization-loop/
访问原文「 JVM进阶 -- 浅谈循环优化 」获取最佳阅读体验并参与讨论