用64个元素声明多个数组比声明65个元素的数组快1000倍

最近我注意到声明一个包含64个元素的数组要比声明65个元素的相同types的数组快很多(> 1000倍)。

这是我用来testing这个的代码:

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[65]replacenew double[64]大约需要7秒。 如果工作分散在越来越多的线程中,这个问题就会成倍地增加,这就是我的问题的起因。

不同types的数组(如int[65]String[65]也会发生此问题。 大string不会出现此问题: String test = "many characters"; ,但是当这被改变成String test = i + "";时开始发生String test = i + "";

我想知道为什么会出现这种情况,是否有可能规避这个问题。

您正在观察由Java VM的JIT编译器所进行的优化所导致的行为。 这种行为是可重复触发的,标量数组多达64个元素,并且不会触发大于64的数组。

在深入细节之前,我们来仔细看看循环的主体:

 double[] test = new double[64]; 

身体没有任何作用(可观察到的行为) 。 这意味着这个语句是否被执行,在程序执行之外没有任何区别。 整个循环也是如此。 所以可能会发生这样的情况:代码优化器将循环转换为具有相同function和不同时序行为的某个(或者什么都不)

对于基准testing,您至less应该遵守以下两条准则。 如果你这样做的话,差距就会小得多。

  • 通过多次执行基准来预热JIT编译器(和优化器)。
  • 使用每个expression式的结果并在基准testing结束时打印出来。

现在让我们进入细节。 毫不奇怪,对于不超过64个元素的标量数组,触发了一个优化。 优化是Escape分析的一部分。 它将小对象和小数组放在堆栈上,而不是将其分配在堆上 – 或者甚至更好地将其完全优化。 您可以在Brian Goetz在2005年撰写的以下文章中find关于它的一些信息:

  • 重访城市performance传奇:分配比你想象的要快,而且变得越来越快

可以使用命令行选项-XX:-DoEscapeAnalysis禁用优化。 标量数组的魔法值64也可以在命令行中更改。 如果你如下执行你的程序,在64和65个元素的数组之间没有区别:

 java -XX:EliminateAllocationArraySizeLimit=65 Tests 

话虽如此,我坚决不鼓励使用这样的命令行选项。 我怀疑它在现实的应用中会产生巨大的影响。 我只会用它,如果我完全相信必要性 – 而不是基于一些伪基准的结果。

根据对象的大小,有多种方式可以有所不同。

正如nosid所述,JITC可能(很可能是)在堆栈上分配小的“本地”对象,“小”数组的大小截止可能是64个元素。

在堆栈上分配比在堆中分配快得多,而且更重要的是,堆栈不需要被垃圾收集,所以GC开销大大降低。 (对于这个testing用例,GC的开销可能是总执行时间的80-90%。)

此外,一旦这个值被堆栈分配,JITC就可以执行“死代码消除”,确定new的结果在任何地方都不会被使用,并且在确保没有任何可能丢失的副作用之后,消除整个new操作,然后(现在是空的)循环本身。

即使JITC没有进行堆栈分配,小于一定大小的对象完全有可能以不同于大的对象的方式分配在一个堆中(例如,从一个不同的“空间”)。 (通常这不会产生如此戏剧性的时间差异,但是。)