在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."<init>:()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),如果程序一直向下线性执行的话,就能够充分地引用流水线,因为下一条执行的指令马上就会就绪。
不过,一旦碰到跳转指令,指令流水线的优势通常就消失了,因为流水线的内容需要丢弃掉并重新从主内存中跳转地址处开始加载新的操作码。这里所产生的性能损耗和缓存未命中是类似的————都要额外从主存中加载一次。
对于前向跳转(注ÿ