循環無關代碼(Loop-invariant Code)外提
如下循環代碼:
package per.william.ex.simd;
public class Foo {
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[]);
descriptor: (II[I)I
flags: (0x0000)
Code:
stack=4, locals=6, args_size=4
0: iconst_0
1: istore 4
3: iconst_0
4: istore 5
//循環體開始
6: iload 5 // load i
8: aload_3 // load a[]
9: arraylength // a.length
10: if_icmpge 32 // i<a.length
13: iload 4 // load sum
15: iload_1 // load x
16: iload_2 // load y
17: imul // x*y
18: aload_3 // load a[]
19: iload 5 // load i
21: iaload // load a[i]
22: iadd // x*y+a[i]
23: iadd // x*y+a[i]+sum
24: istore 4 // sum=sum+x*y+a[i]
26: iinc 5, 1 //i++
29: goto 6
// 循環體結束
32: iload 4
34: ireturn
棧幀的局部變量表如下:
LocalVariableTable:
Start Length Slot Name Signature
6 26 5 i I
0 35 0 this Lper/william/ex/simd/Foo;
0 35 1 x I
0 35 2 y I
0 35 3 a [I
3 32 4 sum I
在上面的循環體中,x*y 和 a.length 屬於循環不變代碼,即循環無關代碼。前者是個乘法運算,後者是內存訪問操作,讀取數組a 的長度(存放在數組對象的對象頭中)。理性情況,上面的循環代碼應該作如下優化:
package per.william.ex.simd;
public class Foo {
int foo(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;
}
}
不過,我通過 JITWatch 查看,貌似只是把 a.length 外提了,x*y 依舊在循環體內(不會彙編,/捂臉):
循環展開(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;
}
在 C2 中,只有計數循環(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 節點數目超過閾值的循環,即時編譯器則不會進行任何循環展開。