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


91

最近,我注意到声明包含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 + "";

我想知道为什么会这样,是否有可能规避这个问题。


3
注释:System.nanoTime()应优先System.currentTimeMillis()选择基准测试。
火箭男孩

4
我只是好奇 ?您是否在Linux下?行为是否随OS改变?
bsd

9
这个问题到底是怎么产生的?
Rohit Jain

2
FWIW,如果我使用byte而不是来运行此代码,则会看到类似的性能差异double
奥利弗·查尔斯沃思

3
@ThomasJungblut:那么,如何解释OP实验中的差异?
奥利弗·查尔斯沃思

Answers:


88

您正在观察到由Java VM的JIT编译器进行的优化所引起的行为。此行为在最多包含64个元素的标量数组中触发时可重现,而在大于64个数组中则不会触发。

在进入细节之前,让我们仔细看一下循环的主体:

double[] test = new double[64];

身体没有作用(可观察到的行为)。这意味着无论是否执行该语句,在程序执行之外都没有区别。整个循环也是如此。因此,代码优化器可能会将循环转换为具有相同功能和不同定时行为的某种东西(或什么都没有)

对于基准测试,您至少应遵守以下两个准则。如果您这样做,则差异会小得多。

  • 通过多次执行基准测试来预热JIT编译器(和优化器)。
  • 使用每个表达式的结果并将其打印在基准测试的末尾。

现在让我们详细介绍。毫不奇怪,对于不大于64个元素的标量数组,会触发优化。优化是Escape分析的一部分。它将小对象和小数组放到堆栈上,而不是在堆上分配它们-甚至更好地完全优化它们。您可以在2005年的Brian Goetz的以下文章中找到有关此信息的一些信息:

可以使用命令行选项禁用优化-XX:-DoEscapeAnalysis。标量数组的魔术值64也可以在命令行上更改。如果按以下方式执行程序,则包含64和65个元素的数组之间将没有区别:

java -XX:EliminateAllocationArraySizeLimit=65 Tests

话虽如此,我强烈不鼓励使用此类命令行选项。我怀疑这在实际应用中会带来巨大的不同。如果我完全确信其必要性,而不是基于某些伪基准测试的结果,我只会使用它。


9
但是,为什么优化程序检测到大小为64的数组是可移动的而不是65
ug_13

10
@nosid:虽然OP的代码可能不切实际,但它显然在JVM中触发了有趣/意外的行为,这可能在其他情况下产生影响。我认为问这是为什么是正确的。
奥利弗·查尔斯沃思

1
@ThomasJungblut我不认为循环会被删除。您可以在循环外添加“ int total”,并添加“ total + = test [0];”。以上示例。然后打印结果,您将看到总数= 1亿,并且在不到一秒的时间内完成了整笔运行。
2013年

1
栈上替换是关于用运行中的编译替换解释后的代码,而不是用栈分配替换堆分配。EliminateAllocationArraySizeLimit是在转义分析中被视为标量可替换的数组的极限大小。因此,影响归因于编译器优化的要点是正确的,但这不是归因于堆栈分配,而是归因于转义分析阶段未能注意到不需要分配。
kiheru

2
@Sipko:您正在编写应用程序无法随线程数扩展的功能。这表明该问题与您要询问的微优化无关。我建议看大图而不是小图。
nosid 2013年

2

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

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

在堆栈上分配的速度比在堆中分配的速度要快得多,而且,更重要的是,不需要对堆栈进行垃圾收集,因此极大地减少了GC开销。(对于此测试用例,GC开销可能占总执行时间的80-90%。)

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

即使JITC不进行堆栈分配,对于小于某个大小的对象,也有可能与较大的对象以不同的方式(例如,来自不同的“空间”)在堆中分配。(不过,通常这不会产生那么大的时序差异。)


迟到此线程。为什么在堆栈上分配比在堆上分配快?根据几篇文章,在堆上分配大约需要12条指令。没有太多的改进空间。
Vortex

@Vortex-分配给堆栈需要1-2条指令。但这就是分配整个堆栈帧。无论如何都必须分配堆栈帧以具有例程的寄存器保存区,因此同时分配的任何其他变量都是“空闲”的。正如我所说,该堆栈不需要GC。堆项的GC开销远大于堆分配操作的开销。
热门点击
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.