在JVM内部实现系列的前几篇文章中,我们已经看到了Java的HotSpot虚拟机的just-in-time (JIT)编译技术,包括逃逸分析和锁消除。本文我们将要讨论另一种自动优化,叫作循环展开。JIT编译器使用这项技术来让循环(比如Java的for或者while循环)执行得更加高效。
由于我们要对JVM的内部机制进行深入分析,所以你会时不时看到用于讲解介绍的各种C的代码甚至是汇编语言,扶稳了!
我们先从下面这段C代码开始,它会去分配100万个long类型的空间,然后用100万个随机的long值来填充。
int main(int argv, char** argc) {
int MAX = 1000000;
long* data = (long*)calloc(MAX, sizeof(long));
for (int i = 0; i < MAX; i++) {
data[i] = randomLong();
}
}
C被认为是一门高级语言,不过事实真的是这样的吗?在苹果Mac电脑上,用Clang编译器(开启—S选项来打印Intel格式的汇编语言)来编译前面的代码会得到如下的输出结果:
_main: ## @main
BB#0:
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
movl $8, %eax
movl %eax, %ecx
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
movl $1000000, -20(%rbp) ## imm = 0xF4240
movslq -20(%rbp), %rdi
movq %rcx, %rsi
callq _calloc
movq %rax, -32(%rbp)
movl $0, -36(%rbp)
LBB1_1: ## LBB1_1是内部循环的Header Depth=1
movl -36(%rbp), %eax
cmpl -20(%rbp), %eax
jge LBB1_4
BB#2: ## 循环体内部: Header=BB1_1 Depth=1
callq _randomLong
movslq -36(%rbp), %rcx
movq -32(%rbp), %rdx
movq %rax, (%rdx,%rcx,8)
BB#3: ## 循环体内部: Header=BB1_1 Depth=1
movl -36(%rbp), %eax
addl $1, %eax
movl %eax, -36(%rbp)
jmp LBB1_1
LBB1_4:
movl -4(%rbp), %eax
addq $48, %rsp
popq %rbp
retq
看下这个代码,你会发现开始处有一次calloc函数的调用,并且仅存在一次randomLong()函数的调用(在循环中)。里面有两次跳转,它和下面变种的C代码所生成的机器代码本质上是一样的:
int main(int argv, char** argc) {
int MAX = 1_000_000;
long* data = (long*)calloc(MAX, sizeof(long));
int i = 0;
LOOP: if (i >= MAX)
goto END;
data[i] = randomLong();
++i;
goto LOOP;
END: return 0;
}
Java里面同样的代码应该是这样的:
public class LoopUnroll {
public static void main(String[] args) {
int MAX = 1000000;
long[] data = new long[MAX];
java.util.Random random = new java.util.Random();
for (int i = 0; i < MAX; i++) {
data[i] = random.nextLong();
}
}
}
编译成字节码的话,就成了这样:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // int 1000000
2: istore_1
3: iload_1
4: newarray long
6: astore_2
7: new #3 // class java/util/Random
10: dup
11: invokespecial #4 // 方法 java/util/Random."😦)V
14: astore_3
15: iconst_0
16: istore 4
18: iload 4
20: iload_1
21: if_icmpge 38
24: aload_2
25: iload 4
27: aload_3
28: invokevirtual #5. // 方法 java/util/Random.nextLong:()J
31: lastore
32: iinc 4, 1
35: goto 18
38: return
这些程序在代码结构来看都非常相似。它们都在循环中对数组data进行了一次操作。真实的处理器会有指令流水线(instruction pipeline),如果程序一直向下线性执行的话,就能够充分地引用流水线,因为下一条执行的指令马上就会就绪。
不过,一旦碰到跳转指令,指令流水线