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

2022-08-31 11:17:38

最近我注意到声明包含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毫秒内运行,如果我用它替换它大约需要7秒。如果作业分布在越来越多的线程上,则此问题将成倍地严重,这就是我的问题的根源。new double[64]new double[65]

不同类型的数组(如 或 )也会出现此问题。对于大字符串,不会出现此问题:,但当它更改为int[65]String[65]String test = "many characters";String test = i + "";

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


答案 1

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

在详细介绍之前,让我们仔细看看循环的主体:

double[] test = new double[64];

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

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

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

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

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

java -XX:EliminateAllocationArraySizeLimit=65 Tests

话虽如此,我强烈建议不要使用这样的命令行选项。我怀疑它在实际应用中会产生巨大的差异。只有当我绝对相信它的必要性时,我才会使用它 - 而不是基于一些伪基准的结果。


答案 2

根据对象的大小,可以通过多种方式进行差异。

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

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

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

即使 JITC 不进行堆栈分配,小于特定大小的对象也完全有可能在堆中分配不同(例如,来自不同的“空间”)。而不是较大的对象。(不过,通常情况下,这不会产生如此巨大的时序差异。