为什么两个相似的循环代码在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");
}

这是第一个版本,在我的电脑上花费了443ms。

第一个版本结果

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

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

我做了以下操作:

  • main方法创建Main

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

  • 在main方法中这样做:

    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可执行文件,而第二次运行时,JVM可执行文件已经缓存在RAM中。或者类似的东西

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

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

1 -这可能是因为解释器收集统计信息的方式。一般的想法是,字节码解释器在诸如分支之类的事情上积累统计信息,直到它"足够"为止。然后JVM触发JIT编译器将字节码编译为本机代码。这样做后,代码的运行速度通常会提高10倍或更多。不同的循环模式是否会达到"足够"?一个版本比另一个版本更早。NB:我只是在猜测。


底线是在编写Java基准测试时必须小心,因为各种JVM预热效果可能会扭曲计时。

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

我自己测试了一下,我得到了相同的差异(大约16ms和4ms)。

经过测试,我发现:

声明1M的变量所花费的时间比乘以1 1M的时间要少。

如何?

我赚了100元

final int nb = 100000000;
for(int i = 1;i<=nb;i++){
i *= 1;
i *= 1;
[... written 20 times]
i *= 1;
i *= 1;
}

final int nb = 100000000;
for(int i = 1;i<=nb;i++){
int a = 0;
int aa = 0;
[... written 20 times]
int aaaaaaaaaaaaaaaaaaaaaa = 0;
int aaaaaaaaaaaaaaaaaaaaaaa = 0;
}

和我分别得到8和3ms,这似乎与你得到的相对应。

使用不同的处理器可以得到不同的结果。

你在算法书第一章找到了答案:

生产和分配成本为1。在第一个算法中,你有2个声明和赋值10000000在第二个算法中,你把它设为100。所以你减少了时间…

in first:主循环5个,次循环3个->第二个循环是:3*100 = 300然后300 + 5 ->305 * 10000000 = 3050000000

in second:3*10000000 = 30000000 ->(30000000 + 5)*100 = 3000000500

所以第二个算法在理论上更快,但我认为它回到了多cpu的…他们可以在第一个做10000000个并行作业但在第二个只能做100个并行作业....所以第一个变快了。

最新更新