循环无关代码外提
外提无关代码
- 循环无关代码:循环中值不变的表达式
- 在不改变程序语义的情况下,将循环无关代码提出循环外
- 那么程序将避免重复执行这些表达式,从而达到性能提升的效果
1 | int foo(int x, int y, int[] a) { |
1 | // 对应的字节码 |
- 循环体中的表达式
x*y
和循环判断条件中的a.length
均属于循环不变代码 x*y
是整数乘法运算,a.length
是内存访问操作,读取数组对象a的长度- 数组的长度存放在数组对象的对象头中,通过
arraylength
指令来访问
- 数组的长度存放在数组对象的对象头中,通过
理想情况下,经过循环无关代码外提后,等同于下面的手工优化版本
1 | int fooManualOpt(int x, int y, int[] a) { |
即时编译器除了将x*y
和a.length
外提,还提供int数组加载指令iaload
所暗含的null check
和range check
,伪代码如下
1 | int iaload(int[] arrayRef, int index) { |
外提null check
foo方法中的null check
属于循环无关代码,即与第几次循环无关,将iaload伪代码展开,得到
1 | int foo(int[] a) { |
在C2中,null check
的外提是通过额外的编译优化(_即循环预测_,-XX:+UseLoopPredicate)来实现的
该优化的实际做法就是在循环之前插入同样的检测代码,并在命中的时候进行去优化
1 | int foo(int[] a) { |
外提range check
由于如果外提range check
之后,将无法再引用到循环变量,因此即时编译器需要转换检测条件
1 | for (int i = INIT; i < LIMIT; i += STRIDE) { |
循环展开
循环展开:在循环体中重复多次循环迭代,并减少循环次数的编译优化,是一种以空间换时间的优化方式
1 | int foo(int[] a) { |
经过一次循环展开后
1 | int foo(int[] a) { |
计数循环
在C2中,只有计数循环才能被展开,计数循环需要满足以下条件
- 维护一个循环计数器,并且基于计数器的循环出口只有一个
- 但可以有基于其他判断条件的出口
- 循环计数器的类型只能为int/short/char
- 每个迭代循环计数器的增量为常数
- 循环计数器的上限和下限是与循环无关的数值
1 | for (int i = START; i < LIMIT; i += STRIDE) { .. } |
优缺点
循环展开的缺点:增加了代码的冗余度,导致所生成机器码的长度大幅上涨
但随着循环体的增大,优化机会也会不断地增加,一旦循环展开能触发进一步的优化,总体的代码复杂度也将降低
1 | int foo(int[] a) { |
完全展开
完全展开:当循环的数量是固定值而且非常小,即时编译器将循环完全展开
原本循环中的循环判断语句将不复存在,变成了若干顺序执行的循环体
1 | int foo(int[] a) { |
完全展开
1 | int foo(int[] a) { |
即时编译器会在循环体的大小与循环展开次数之间作出权衡
循环判断外提
循环判断外提:将循环中的if语句外提到循环之前,并且在该if语句的两个分支中分别放置一份循环代码
1 | int foo(int[] a) { |
1 | // 循环判断外提 |
循环判断外提与循环无关检测外提所针对的代码模式比较类似,都是循环中的if语句
循环无关检测外提在检查失败时会抛出异常,中止当前的正常执行路径
循环判断外提针对更常见的情况,即通过if语句的不同分支执行不同的代码逻辑
循环剥离
循环剥离:将循环的前几个迭代或者后几个迭代剥离出循环的优化方式
通过将这几个特殊的迭代剥离出去,使得原本的循环体的规律性更加明显,从而触发进一步优化
1 | int foo(int[] a) { |
剥离第一个迭代
1 | int foo(int[] a) { |