JVM优化之循环展开

在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 &gt;= 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&gt;:()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),如果程序一直向下线性执行的话,就能够充分地引用流水线,因为下一条执行的指令马上就会就绪。

不过,一旦碰到跳转指令,指令流水线的优势通常就消失了,因为流水线的内容需要丢弃掉并重新从主内存中跳转地址处开始加载新的操作码。这里所产生的性能损耗和缓存未命中是类似的————都要额外从主存中加载一次。

对于前向跳转(注ÿ

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值