一切伟大的行动和思想,都有一个微不足道的开始 -- 佚名
我们代码开发中会使用到很多的循环,为了提高效率,循环在JVM里面也进行了优化。

循环优化分四种:
1.循环无关代码(Loop-invariant Code)外提
所谓的循环无关代码(Loop-invariant Code),指的是循环中值不变的表达式。如果能够在不改变程序语义的情况下,将这些循环无关代码提出循环之外,那么程序便可以避免重复执行这些表达式,从而达到性能提升的效果。
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;
}
举个例子,在上面这段代码中,循环体中的表达式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,现在都在循环之前完成。原本循环中需要执行这两个表达式的地方,现在直接使用循环之前这两个表达式的执行结果。
2.循环展开(Loop Unrolling)
它指减少迭代次数,在循环体中重复多次循环的迭代,例如:
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;
}
只有计数循环(Counted Loop)才能展开:
维护一个循环计数器,并且基于计数器的出口只有一个。
循环计数器的类型只能是 int 、short 或者 char 。
每个迭代循环计数器的增量是常数。
循环计数器的上限或者下限是循环无关的数值。
循环展开有一种特殊情况,那便是完全展开(Full Unroll)。当循环的数目是固定值而且非常小时,即时编译器会将循环全部展开。此时,原本循环中的循环判断语句将不复存在,取而代之的是若干个顺序执行的循环体。
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;
}
即时编译器会在循环体的大小与循环展开次数之间做出权衡。例如,对于仅迭代三次(或以下)的循环,即时编译器将进行完全展开;对于循环体 IR 节点数目超过阈值的循环,即时编译器则不会进行任何循环展开。
3.循环判断(Loop unswitching)外提
循环判断外提指的是将循环中的 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 语句的不同分支执行不同的代码逻辑。
4.循环剥离(Loop peeling)
循环剥离指的是将循环的前几个迭代或者后几个迭代剥离出循环的优化方式。一般来说,循环的前几个迭代或者后几个迭代都包含特殊处理。通过将这几个特殊的迭代剥离出去,可以使原本的循环体的规律性更加明显,从而触发进一步的优化。
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;
}
以上就是循环优化的四种方式。

