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");
    }
}

如果我替换它大约需要 7 秒,这将在大约 6 毫秒new double[64]new double[65]运行。如果作业分布在越来越多的线程上,这个问题就会变得更加严重,这就是我的问题的根源。

不同类型的数组(例如int[65]或)也会出现此问题String[65]。大字符串不会出现此问题:String test = "many characters";,但在将其更改为时确实开始出现String test = i + "";

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

4

2 回答 2

88

您正在观察由 Java VM 的 JIT 编译器完成的优化引起的行为。此行为可通过最多 64 个元素的标量数组触发,并且不会由大于 64 的数组触发。

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

double[] test = new double[64];

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

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

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

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

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

java -XX:EliminateAllocationArraySizeLimit=65 Tests

话虽如此,我强烈反对使用这样的命令行选项。我怀疑它是否会对实际应用产生巨大影响。我只会使用它,如果我绝对相信必要性 - 而不是基于一些伪基准的结果。

于 2013-09-15T09:20:33.070 回答
2

根据对象的大小,有多种方法可以产生差异。

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

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

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

即使 JITC 不进行堆栈分配,也完全有可能将小于某个大小的对象以不同于较大对象的方式(例如,从不同的“空间”)分配到堆中。(不过,通常这不会产生如此显着的时间差异。)

于 2013-09-15T13:29:55.960 回答