Java 声明包含64个元素的多个数组比声明包含65个元素的数组快1000倍
最近我注意到,声明包含64个元素的数组比声明包含65个元素的相同类型的数组快得多(>1000倍) 下面是我用来测试的代码:Java 声明包含64个元素的多个数组比声明包含65个元素的数组快1000倍,java,arrays,Java,Arrays,最近我注意到,声明包含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++){
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");
}
}
公共类测试{
公共静态void main(字符串参数[]){
双启动=System.nanoTime();
int job=100000000;//1亿
for(int i=0;i
这大约需要6毫秒,如果我用new double[65]
替换new double[64]
,大约需要7秒。如果作业分布在越来越多的线程上,那么这个问题将以指数形式变得更加严重,这就是我的问题的根源
不同类型的数组也会出现此问题,例如int[65]
或String[65]
。
大字符串不会出现此问题:String test=“许多字符”代码>,但当它更改为String test=i+''时开始出现代码>
我想知道为什么会出现这种情况,以及是否有可能避免这个问题。您正在观察由Java VM的JIT编译器进行的优化所导致的行为。此行为可在多达64个元素的标量数组中重复触发,而在大于64个元素的数组中不会触发
在详细讨论之前,让我们仔细看看循环体:
double[] test = new double[64];
身体没有影响(可观察到的行为)。这意味着在程序执行之外,是否执行该语句没有任何区别。整个循环也是如此。因此,代码优化器可能会将循环转换为具有相同功能和不同计时行为的内容(或不转换)
对于基准测试,您至少应该遵守以下两条准则。如果你这样做了,差别会小得多
- 通过多次执行基准测试来预热JIT编译器(和优化器)
- 使用每个表达式的结果,并在基准测试结束时打印它李>
现在让我们来详细讨论一下。毫不奇怪,对于不大于64个元素的标量数组会触发优化。优化是整个过程的一部分。它将小对象和小数组放在堆栈上,而不是在堆上分配它们——或者更好地完全优化它们。您可以在Brian Goetz于2005年撰写的以下文章中找到一些关于它的信息:
可以使用命令行选项-XX:-doescapesanalysis
禁用优化。也可以在命令行上更改标量数组的魔术值64。如果按如下方式执行程序,则具有64个和65个元素的数组之间将没有区别:
java -XX:EliminateAllocationArraySizeLimit=65 Tests
话虽如此,我强烈反对使用此类命令行选项。我怀疑它在实际应用中是否会产生巨大的差异。我只会使用它,如果我绝对相信它的必要性——而不是基于一些伪基准测试的结果。有很多方法可以根据对象的大小产生差异
正如nosid所述,JITC可能(很可能是)在堆栈上分配小的“本地”对象,“小”数组的大小截止值可能是64个元素
在堆栈上分配要比在堆中分配快得多,更重要的是,堆栈不需要进行垃圾收集,因此GC开销大大减少。(对于这个测试用例,GC开销可能占总执行时间的80-90%。)
此外,一旦堆栈分配了值,JITC可以执行“死代码消除”,确定new
的结果从未在任何地方使用过,并且在确保不会丢失任何副作用后,消除整个new
操作,然后(现在为空)循环本身
即使JITC不进行堆栈分配,小于某个大小的对象在堆中的分配也完全有可能与较大的对象不同(例如,从不同的“空间”)。(通常这不会产生如此显著的时间差异。)请注意:System.nanoTime()
应该优先于System.currentTimeMillis()
进行基准测试。我只是好奇?你在Linux下吗?这种行为会随着操作系统的改变而改变吗?这个问题到底是怎么被否决的呢,如果我用byte
而不是double
@ThomasJungblut运行此代码,我会看到类似的性能差异。那么,OP的实验中出现这种差异的原因是什么呢?但为什么优化器会检测到大小为64的数组是可移动的,而不是不可移动的65@nosid:虽然OP的代码可能不现实,它显然在JVM中触发了一个有趣的/意外的行为,这可能会在其他情况下产生影响。我想问一下为什么会发生这种情况是有道理的。@ThomasJungblut我认为循环不会被移除。您可以在循环外部添加“int-total”,并在上面的示例中添加“total+=test[0];”。然后打印结果,您将看到total=1亿,它将在不到一秒钟的时间内运行。堆栈上的替换是将解释代码替换为动态编译的代码,而不是将堆分配替换为堆栈分配。EliminateLocationArraySizeLimit是在逃逸分析中被视为标量可替换的数组的限制大小。因此,主要的一点是由于编译器优化造成的效果是正确的,但这不是由于堆栈分配,而是由于转义分析阶段没有注意到分配不是ne