首页 > 解决方案 > 为什么 2 个相似的循环代码在 java 中花费不同的时间

问题描述

我对以下代码感到困惑:

 public static void test(){
        long currentTime1 = System.currentTimeMillis();

        final int iBound = 10000000;
        final int jBound = 100;
        for(int i = 1;i<=iBound;i++){
            int a = 1;
            int tot = 10;
            for(int j = 1;j<=jBound;j++){
                tot *= a;
            }
        }


        long updateTime1 = System.currentTimeMillis();
        System.out.println("i:"+iBound+" j:"+jBound+"\nIt costs "+(updateTime1-currentTime1)+" ms");
    }

那是第一个版本,在我的电脑上花费了 443 毫秒。

第一版结果

 public static void test(){
        long currentTime1 = System.currentTimeMillis();

        final int iBound = 100;
        final int jBound = 10000000;
        for(int i = 1;i<=iBound;i++){
            int a = 1;
            int tot = 10;
            for(int j = 1;j<=jBound;j++){
                tot *= a;
            }
        }


        long updateTime1 = System.currentTimeMillis();
        System.out.println("i:"+iBound+" j:"+jBound+"\nIt costs "+(updateTime1-currentTime1)+" ms");
    }

第二个版本花费 832ms。 第二个版本结果 唯一的区别是我只是交换了 i 和 j。

这个结果令人难以置信,我在 C 中测试了相同的代码,而 C 中的差异并没有那么大。

为什么这两个相似的代码在java中如此不同?

我的jdk版本是openjdk-14.0.2

标签: java

解决方案


TL;DR - 这只是一个糟糕的基准。

我做了以下事情:

  • 用方法创建一个Mainmain

  • 将两个版本的测试复制为test1()test2()

  • 在主要方法中这样做:

    while(true) {
        test1();
        test2();
    }
    

这是我得到的输出(Java 8)。

i:10000000 j:100
It costs 35 ms
i:100 j:10000000
It costs 33 ms
i:10000000 j:100
It costs 33 ms
i:100 j:10000000
It costs 25 ms
i:10000000 j:100
It costs 0 ms
i:100 j:10000000
It costs 0 ms
i:10000000 j:100
It costs 0 ms
i:100 j:10000000
It costs 0 ms
i:10000000 j:100
It costs 0 ms
i:100 j:10000000
It costs 0 ms
i:10000000 j:100
It costs 0 ms
....

如您所见,当我在同一个 JVM 中交替运行同一个方法的两个版本时,每个方法的时间大致相同。

但更重要的是,经过少量迭代后,时间降至……零!发生的事情是 JIT 编译器编译了这两种方法,并且(可能)推断出它们的循环可以被优化掉。

目前尚不完全清楚为什么当两个版本分别运行时人们会得到不同的时间。一种可能的解释是,第一次运行时,JVM 可执行文件正在从磁盘中读取,而第二次已经缓存在 RAM 中。或类似的东西。

另一种可能的解释是,JIT 编译在较早的1中使用一个版本,test()因此在较慢的解释(预 JIT)阶段花费的时间比例在两个版本之间是不同的。(可以使用 JIT 日志记录选项来解决这个问题。)

但这实际上并不重要……因为 Java 应用程序在 JVM 预热时的性能(加载代码、JIT 编译、将堆增加到其工作大小、加载缓存等)通常来说并不重要。并且对于重要的情况,寻找可以进行 AOT 编译的 JVM;例如 GraalVM。

1 - 这可能是因为解释器收集统计数据的方式。一般的想法是字节码解释器会累积诸如分支之类的统计信息,直到它“足够”为止。然后 JVM 触发 JIT 编译器将字节码编译为本机代码。完成后,代码的运行速度通常会快 10 倍或更多倍。与另一个版本相比,不同的循环模式可能在一个版本中更早地达到“足够”。注意:我在这里推测。我提供零证据...


最重要的是,在编写 Java 基准测试时必须小心,因为各种 JVM 预热效应会扭曲时间。

有关更多信息,请阅读:如何在 Java 中编写正确的微基准测试?


推荐阅读