最近我注意到声明一个包含64个元素的数组比用65个元素声明相同类型的数组快得多(> 1000倍)。
以下是我用来测试此代码的代码:
public class Tests{
public static void main(String args[]){
double start = System.nanoTime();
int job = 100000000;//100 million
for(int i = 0; i < job; i++){
double[] test = new double[64];
}
double end = System.nanoTime();
System.out.println("Total runtime = " + (end-start)/1000000 + " ms");
}
}
这大约需要6毫秒,如果我用new double[64]
替换new double[65]
,大约需要7秒。如果作业分布在越来越多的线程中,这个问题会变得更加严重,这是我的问题所在。
对于int[65]
或String[65]
等不同类型的数组,也会出现此问题。
大字符串不会出现此问题:String test = "many characters";
,但在将其更改为String test = i + "";
时会开始发生
我想知道为什么会这样,如果有可能绕过这个问题。
答案 0 :(得分:88)
您正在观察由Java VM的JIT编译器执行的优化导致的行为。此行为可通过最多64个元素的标量数组触发,并且不会被大于64的数组触发。
在详细介绍之前,让我们仔细看看循环体:
double[] test = new double[64];
正文无效(可观察行为)。这意味着无论是否执行此语句,它在程序执行之外都没有区别。整个循环也是如此。所以可能会发生,代码优化器将循环转换为具有相同功能和不同时序行为的(或没有)。
对于基准测试,您至少应遵循以下两条准则。如果你这样做了,差异就会明显变小。
现在让我们详细介绍一下。毫不奇怪,对于不超过64个元素的标量数组,会触发优化。优化是{{3}}的一部分。它将小对象和小数组放入堆栈而不是在堆上分配它们 - 甚至更好地完全优化它们。您可以在Brian Goetz于2005年撰写的以下文章中找到有关它的一些信息:
可以使用命令行选项-XX:-DoEscapeAnalysis
禁用优化。标量数组的魔术值64也可以在命令行中更改。如果按如下方式执行程序,则具有64和65个元素的数组之间没有区别:
java -XX:EliminateAllocationArraySizeLimit=65 Tests
话虽如此,我强烈反对使用这样的命令行选项。我怀疑它在现实应用中有很大的不同。我只会使用它,如果我绝对相信必要性 - 而不是基于某些伪基准测试的结果。
答案 1 :(得分:2)
根据对象的大小,可以有多种方式存在差异。
正如nosid所说,JITC可能(很可能是)在堆栈上分配小的“本地”对象,而“小”数组的大小截止可能是64个元素。
在堆栈上分配比在堆中分配要快得多,而且更重要的是,堆栈不需要进行垃圾回收,因此GC开销大大降低。 (对于此测试用例,GC开销可能占总执行时间的80-90%。)
此外,一旦值被堆栈分配,JITC就可以执行“死代码消除”,确定new
的结果从未在任何地方使用,并且在确保没有任何副作用之后丢失,消除整个new
操作,然后消除(现在为空)循环本身。
即使JITC不进行堆栈分配,对于小于特定大小的对象,完全有可能在堆中以不同的方式(例如,来自不同的“空间”)而不是更大的对象来分配。 (通常情况下,这不会产生如此戏剧性的时间差异。)