我被下面的代码弄糊涂了:
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个并行作业....所以第一个变快了。