Arrays中的远古智慧

副标题Collection.toArray(new T[0]) 还是 Collection.toArray(new T[size])? 这是个问题。

前言

这篇文章翻译自Arrays of Wisdom of the Ancients。上个星期同事在看阿里巴巴Java开发手册(详尽版)时,看到了这一条规则:

Java开发手册

出于好奇google了集合转数组最佳实践相关的说法,发现了Arrays of Wisdom of the Ancients这一篇文章。这是一篇篇幅很长的文章,作者通过一系列实验后得出的最后结论是反直觉的:toArray(new T[0])比toArray(new T[list.size()])更高效。

为什么要翻译这篇文章?

作为一名后端工程师,我接触过很多工具,不管用的是开源工具或自研系统都可能需要根据自身的应用场景做系统调优。为了调优做大大小小的微基准测试、压力测试、集成测试也是非常常见的。
尽管做过很多次基准测试,但很多时候得到结果就算完成了,只知其然,不知其所以然。这篇文章让我很欣赏的地方在于:它是一系列完整的实验,有严谨的实验过程,从立论(凭直觉猜测结果)、如何论证(设计实验,用什么工具、什么方法证明或证反)到最后总结定论。也介绍了多种用于分析微基准测试的工具。翻译出来是希望更多人看到它,学习优秀的性能测试方法。然而这篇文章涉及了许多底层的知识,比如JVM是如何分配数组对象的、cpu offload 、JNI调用、JVM编译生成汇编代码...等。由于自身水平有限,可能存在翻译错误、曲解了作者的意思的情况,强烈建议有兴趣的同学直接看原文。下面是翻译的正文。

介绍

Java语言和JDK Class Library有两种不同但有关联的方式来把元素组合在一起:arrays和Collections。两者都各有优劣,因此两者在真实的程序中都很常见。为了帮助实现两种类型数据的互相转换,jdk提供了把array转换成Collection(e.g. Arrays.asList)和把Collection复制到Array(e.g. 好几个Collection.toArray的方法)的标准实现。这篇文章中,我们尝试回答一个有争议的问题:哪种toArray方式更高效?

API设计

盲目调用Collection中的toArray 、或者遵循我们从其它工具或google搜索中得到的任何建议似乎是件很平常的事情 。但是,如果我们查看Collection.toArray系列方法,我们可以看到两个独立的方法:

public interface Collection<E> extends Iterable<E> {

    /**
     * Returns an array containing all of the elements in this collection.
     *
     * ...
     *
     * @return an array containing all of the elements in this collection
     */
    Object[] toArray();

    /**
     * Returns an array containing all of the elements in this collection;
     * the runtime type of the returned array is that of the specified array.
     * If the collection fits in the specified array, it is returned therein.
     * Otherwise, a new array is allocated with the runtime type of the
     * specified array and the size of this collection.
     *
     * ...
     *
     * @param <T> the runtime type of the array to contain the collection
     * @param a the array into which the elements of this collection are to be
     *        stored, if it is big enough; otherwise, a new array of the same
     *        runtime type is allocated for this purpose.
     * @return an array containing all of the elements in this collection
     * @throws ArrayStoreException if the runtime type of the specified array
     *         is not a supertype of the runtime type of every element in
     *         this collection
     */
    <T> T[] toArray(T[] a);

这些方法表现得略微不同是有一定原因的。泛型类型擦除造成的性能影响迫使我们使用实际的参数来精确解释目标数组类型。另外,简单地将toArray()返回的Object[]转换为ConcreteType[]是不可行的,因为运行时必须保证类型安全,像这样的数组类型转换将导致ClassCastException。这么做可以避免恶意代码通过在Object[]数组中放置非ConcreteType来规避类型安全。把数组作为参数的toArray方法可以把结果都放在一个预先设置好长度的数组中。

事实上,古人的智慧可能会告诉我们,为了获得最好的性能,我们应该提供预先设置好长度的数组(甚至是长度为零的数组!)。IntelliJIDEA 15建议传递预先分配好长度的数组,而不是偷懒传一个长度为零的数组。它解释说,Library必需通过反射调用才能分配给定的运行时类型数组,这显然要让你付出代价。而PMD的OptimizableToArrayCall规则告诉我们同样的情况,但似乎也暗示新分配的“空”数组会因为长度不足而被丢弃,我们应该通过传递预先分配长度的目标数组来避免这种情况。古人到底有多聪明?


Figure 1. IntelliJ IDEA 15 tries to help us
Figure 2. PMD 5.4.1 tries to help me

性能测试

实验设计

在开始实验之前,我们需要先了解这个实验的自由度。至少有三个条件需要考虑:

  1. 集合大小。 PMD的规则告诉我们分配会被丢弃的数组(长度过短的数组)毫无意义。这意味着我们的实验需要覆盖一些小集合,以查看“无偿”数组实例化的影响。我们还希望看到大集合的性能,而我们预计元素复制成本是影响性能的主要因素。当然,我们还要测试长度位于两者之间的值,以避免掉进“性能甜蜜点”。
  2. toArray()参数的类型。当然,我们希望尝试toArray方法调用的所有变体。尤其是零大小数组调用与预先分配大小的数组调用,但非类型化的toArray方法也很有趣。
  3. 集合中的toArray()实现。IDEA认为,反射数组实例化可能要付出昂贵代价。因此,我们需要调查实际使用的集合的实现逻辑。大多数集合与抽象集合相同:在后一种情况下分配Object[]或T[]数组 ,实际上,就是使用java.lang.Reflt.Array:newInstance方法 - 分配T[]数组,然后通过迭代器,逐个将元素复制到目标数组中。
public abstract class AbstractCollection {
  public <T> T[] toArray(T[] a) {
    int size = size();
    T[] r = (a.length >= size) ?
             a : (T[])Array.newInstance(a.getClass().getComponentType(), size);
    Iterator<E> it = iterator();
    for (int i = 0; i < r.length; i++) {
      ...
      r[i] = (T)it.next();
    }
    return ... r;
  }
}

有的集合,比如 ArrayList, 只是简单地把他们的内部成员变量数组复制到目标数组中。

public class ArrayList {
  public <T> T[] toArray(T[] a) {
    if (a.length < size) {
      // Arrays.copyOf would do the Array.newInstance
      return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    }
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size) {
      a[size] = null;
    }
    return a;
  }
}

ArrayList是最常用的集合之一,因此我们想看看无处不在的ArrayList和类似HashSet这样的通用的、基于抽象集合toArray()方法支持的集合是如何执行的。

基准测试

根据上述观察,我们制定了如下的JMH基准测试

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgsAppend = {"-XX:+UseParallelGC", "-Xms1g", "-Xmx1g"})
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class ToArrayBench {

    @Param({"0", "1", "10", "100", "1000"})
    int size;

    @Param({"arraylist", "hashset"})
    String type;

    Collection<Foo> coll;

    @Setup
    public void setup() {
        if (type.equals("arraylist")) {
            coll = new ArrayList<Foo>();
        } else if (type.equals("hashset")) {
            coll = new HashSet<Foo>();
        } else {
            throw new IllegalStateException();
        }
        for (int i = 0; i < size; i++) {
            coll.add(new Foo(i));
        }
    }

    @Benchmark
    public Object[] simple() {
        return coll.toArray();
    }

    @Benchmark
    public Foo[] zero() {
        return coll.toArray(new Foo[0]);
    }

    @Benchmark
    public Foo[] sized() {
        return coll.toArray(new Foo[coll.size()]);
    }

    public static class Foo {
        private int i;

        public Foo(int i) {
            this.i = i;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Foo foo = (Foo) o;
            return i == foo.i;
        }

        @Override
        public int hashCode() {
            return i;
        }
    }
}

实验结果

实验运行环境为 i7-4790K, 4.0 GHz, Linux x86_64, JDK 9b99 EA ,我们得到如下实现结果(平均运行时间,数值越小越好):

Benchmark            (size)     (type)  Mode  Cnt      Score    Error  Units

# ---------------------------------------------------------------------------

ToArrayBench.simple       0  arraylist  avgt   30    19.445 ±   0.152  ns/op
ToArrayBench.sized        0  arraylist  avgt   30    19.009 ±   0.252  ns/op
ToArrayBench.zero         0  arraylist  avgt   30     4.590 ±   0.023  ns/op

ToArrayBench.simple       1  arraylist  avgt   30     7.906 ±   0.024  ns/op
ToArrayBench.sized        1  arraylist  avgt   30    18.972 ±   0.357  ns/op
ToArrayBench.zero         1  arraylist  avgt   30    10.472 ±   0.038  ns/op

ToArrayBench.simple      10  arraylist  avgt   30     8.499 ±   0.049  ns/op
ToArrayBench.sized       10  arraylist  avgt   30    24.637 ±   0.128  ns/op
ToArrayBench.zero        10  arraylist  avgt   30    15.845 ±   0.075  ns/op

ToArrayBench.simple     100  arraylist  avgt   30    40.874 ±   0.352  ns/op
ToArrayBench.sized      100  arraylist  avgt   30    93.170 ±   0.379  ns/op
ToArrayBench.zero       100  arraylist  avgt   30    80.966 ±   0.347  ns/op

ToArrayBench.simple    1000  arraylist  avgt   30   400.130 ±   2.261  ns/op
ToArrayBench.sized     1000  arraylist  avgt   30   908.007 ±   5.869  ns/op
ToArrayBench.zero      1000  arraylist  avgt   30   673.682 ±   3.586  ns/op

# ---------------------------------------------------------------------------

ToArrayBench.simple       0    hashset  avgt   30    21.270 ±   0.424  ns/op
ToArrayBench.sized        0    hashset  avgt   30    20.815 ±   0.400  ns/op
ToArrayBench.zero         0    hashset  avgt   30     4.354 ±   0.014  ns/op

ToArrayBench.simple       1    hashset  avgt   30    22.969 ±   0.221  ns/op
ToArrayBench.sized        1    hashset  avgt   30    23.752 ±   0.503  ns/op
ToArrayBench.zero         1    hashset  avgt   30    23.732 ±   0.076  ns/op

ToArrayBench.simple      10    hashset  avgt   30    39.630 ±   0.613  ns/op
ToArrayBench.sized       10    hashset  avgt   30    43.808 ±   0.629  ns/op
ToArrayBench.zero        10    hashset  avgt   30    44.192 ±   0.823  ns/op

ToArrayBench.simple     100    hashset  avgt   30   298.032 ±   3.925  ns/op
ToArrayBench.sized      100    hashset  avgt   30   316.250 ±   9.614  ns/op
ToArrayBench.zero       100    hashset  avgt   30   284.431 ±   6.201  ns/op

ToArrayBench.simple    1000    hashset  avgt   30  4227.564 ±  84.983  ns/op
ToArrayBench.sized     1000    hashset  avgt   30  4539.614 ± 135.379  ns/op
ToArrayBench.zero      1000    hashset  avgt   30  4428.601 ± 205.191  ns/op

# ---------------------------------------------------------------------------

实验结果表明,simple 性能完胜其它实现,然而,反直觉的是,zero 竟然比sized 性能更好。

实验到这个阶段,大部分人都会犯这样的错误:把这些数据当成事实。但这些数据只是数据,它们不能说明任何东西,除非我们明白得到这些结果的原因。

仔细观察这些数据,我们想得到两个主要问题的答案:

  1. 为什么simplezerosized都快
  2. 为什么zerosized快?

解决这两个问题是让我们理解发生了什么的关键

对于任何优秀的性能工程师来说,在开始调查之前应该尝试猜出答案、然后找出实际答案,来训练直觉。花几分钟给这些问题找出合理的假设答案。你要做什么实验来证实这些假设?什么样的实验能够证伪它们?

不是分配压力

先来一个简单的假设:分配压力。人们会猜测不同的分配压力会造成不同的性能影响。事实上,一些GC敏感的服务同样是GC受限的,即性能瓶颈是GC。

通常基准测试GC限制的服务是很简单的。在我们的场景下,我们用单线程来跑基准测试代码,这使得GC线程可以自由地在单独的核上运行,使用多个GC线程来处理单个基准测试线程的垃圾。添加更多的基准测试线程将使它们:a)与GC线程竞争CPU时间,从而隐式地影响GC时间;b)产生更多的垃圾,从而使每个应用程序线程的GC线程的有效数量下降,加剧内存管理成本。

这是我们通常希望在单线程和多线程(饱和)模式下运行基准测试的原因之一:在饱和模式下运行可以捕获系统正在执行的任何“隐藏”cpu offload活动。

但在我们的例子中,我们可以走捷径直接估计分配压力。因为,JMH有一个-prof gc分析器,它侦听gc事件,将它们相加,并将分配/流失率归一化为基准操作数,从而为每个@Benchmark调用提供分配压力。

Allocation pressure (the table shows only "gc.alloc.rate.norm" metric)
Benchmark           (size)     (type)  Mode  Cnt    Score    Error  Units

# ------------------------------------------------------------------------

ToArrayBench.simple      0  arraylist  avgt   30    16.000 ± 0.001  B/op
ToArrayBench.sized       0  arraylist  avgt   30    16.000 ± 0.001  B/op
ToArrayBench.zero        0  arraylist  avgt   30    16.000 ± 0.001  B/op

ToArrayBench.simple      1  arraylist  avgt   30    24.000 ± 0.001  B/op
ToArrayBench.sized       1  arraylist  avgt   30    24.000 ± 0.001  B/op
ToArrayBench.zero        1  arraylist  avgt   30    40.000 ± 0.001  B/op

ToArrayBench.simple     10  arraylist  avgt   30    56.000 ± 0.001  B/op
ToArrayBench.sized      10  arraylist  avgt   30    56.000 ± 0.001  B/op
ToArrayBench.zero       10  arraylist  avgt   30    72.000 ± 0.001  B/op

ToArrayBench.simple    100  arraylist  avgt   30   416.000 ± 0.001  B/op
ToArrayBench.sized     100  arraylist  avgt   30   416.000 ± 0.001  B/op
ToArrayBench.zero      100  arraylist  avgt   30   432.000 ± 0.001  B/op

ToArrayBench.simple   1000  arraylist  avgt   30  4016.001 ± 0.001  B/op
ToArrayBench.sized    1000  arraylist  avgt   30  4016.001 ± 0.002  B/op
ToArrayBench.zero     1000  arraylist  avgt   30  4032.001 ± 0.001  B/op

# ------------------------------------------------------------------------

ToArrayBench.simple      0    hashset  avgt   30    16.000 ± 0.001  B/op
ToArrayBench.sized       0    hashset  avgt   30    16.000 ± 0.001  B/op
ToArrayBench.zero        0    hashset  avgt   30    16.000 ± 0.001  B/op

ToArrayBench.simple      1    hashset  avgt   30    24.000 ± 0.001  B/op
ToArrayBench.sized       1    hashset  avgt   30    24.000 ± 0.001  B/op
ToArrayBench.zero        1    hashset  avgt   30    24.000 ± 0.001  B/op

ToArrayBench.simple     10    hashset  avgt   30    56.000 ± 0.001  B/op
ToArrayBench.sized      10    hashset  avgt   30    56.000 ± 0.001  B/op
ToArrayBench.zero       10    hashset  avgt   30    56.000 ± 0.001  B/op

ToArrayBench.simple    100    hashset  avgt   30   416.000 ± 0.001  B/op
ToArrayBench.sized     100    hashset  avgt   30   416.001 ± 0.001  B/op
ToArrayBench.zero      100    hashset  avgt   30   416.001 ± 0.001  B/op

ToArrayBench.simple   1000    hashset  avgt   30  4056.006 ± 0.009  B/op
ToArrayBench.sized    1000    hashset  avgt   30  4056.007 ± 0.010  B/op
ToArrayBench.zero     1000    hashset  avgt   30  4056.006 ± 0.009  B/op

# ------------------------------------------------------------------------

实验结果表示,分配压力在三种toArray方法中几乎没有差别。zero在一些组别中比其它两个多16byte , 这是"redundant" array allocation导致的。(给读者的练习:为什么zero在Hashset的表现中和其它两个一致?)。但是,我们之前的吞吐量基准测试的结果是zero更快,而不是慢,正如分配压力假说所预测的那样。因此,分配压力不能解释我们看到的现象。

性能分析

在我们的领域里,我们可以跳过建立假设, 直接用厉害的工具来进行分析。如果我们用后见之明,直接得出结论,这一节可能会更短。但这篇文章的重点之一是展示分析这些基准的方法。

Meet VisualVM (and other Java-only profilers)

最明显的方法就是用java profiler连接一个基准测试中的JVM,看看里面发生了什么。在保证通用性的前提下,我们以JDK自带的VisualVM分析器为例,这样大多数安装了java的环境都可以使用它。

VisualVM使用起来非常简单:开启进程,开启VisualVM(如果JDK在你的PATH环境变量里,直接使用jvisualvm即可), 从列表中选择一个VM,然后点击"Sample"->"CPU Sampling"采集,然后享受结果吧。下图是我们的实验结果:

Figure 3. VisualVM profile snapshot for the "ToArrayBench.simple" case and ArrayList of size 1000

Emmm... 信息量很大。

大多数java profilers都有内部偏见,因为它们要么检测代码,从而推测真实结果,要么在代码中指定的位置(例如safepoints)取样,从而也会歪曲结果。在我们上面的例子中,虽然大部分工作是在simple()方法中完成的,但profiler将工作误归为持有基准循环的…_jmhStub方法所有。
但这不是核心问题。对我们来说,最成问题的部分是缺少任何可以帮助我们回答性能问题的低级细节。你能在上面的分析快照中看到任何可以验证我们案例中任何合理假设的东西吗? 不能,因为数据太粗糙了。注意,在更高的工作负载下,偏见效应影响可忽略不计。

Meet JMH -prof perfasm

微基准测试工具为探索微基准测试底层细节的需求提供了明确的分析、检验方法。比如JMH,有一个内嵌的"perfasm" 分析器用于从VM转储PringAssembly 、添加perf_event计数器注释、打印热点。perf_event提供了一个基于硬件计数器的非侵入式的采样分析器,正是我们对细粒度性能工程的要求。

下面是实验的输出结果

$ java -jar target/benchmarks.jar zero -f 1 -p size=1000 -p type=arraylist -prof perfasm
....[Hottest Region 1].......................................................................
 [0x7fc4c180916e:0x7fc4c180920c] in StubRoutines::checkcast_arraycopy

StubRoutines::checkcast_arraycopy [0x00007fc4c18091a0, 0x00007fc4c180926b]
  0.04%                  0x00007fc4c18091a0: push   %rbp
  0.06%    0.01%         0x00007fc4c18091a1: mov    %rsp,%rbp
  0.01%                  0x00007fc4c18091a4: sub    $0x10,%rsp
  0.02%    0.01%         0x00007fc4c18091a8: mov    %r13,(%rsp)
  0.14%    0.02%         0x00007fc4c18091ac: mov    %r14,0x8(%rsp)
  0.07%    0.02%         0x00007fc4c18091b1: lea    (%rdi,%rdx,4),%rdi
  0.01%                  0x00007fc4c18091b5: lea    (%rsi,%rdx,4),%r13
                         0x00007fc4c18091b9: mov    %rdx,%r14
  0.02%                  0x00007fc4c18091bc: neg    %rdx
  0.01%    0.01%   ╭     0x00007fc4c18091bf: jne    0x00007fc4c18091de
                   │     0x00007fc4c18091c5: xor    %rax,%rax
                   │     0x00007fc4c18091c8: jmpq   0x00007fc4c1809260
                   │     0x00007fc4c18091cd: data16 xchg %ax,%ax
 19.30%   19.73%   │↗↗↗  0x00007fc4c18091d0: mov    %eax,0x0(%r13,%rdx,4)
  5.96%    6.89%   ││││  0x00007fc4c18091d5: inc    %rdx
                   ││││  0x00007fc4c18091d8: je     0x00007fc4c1809233
  3.84%    4.92%   ↘│││  0x00007fc4c18091de: mov    (%rdi,%rdx,4),%eax
  5.83%    6.56%    │││  0x00007fc4c18091e1: test   %rax,%rax
                    ╰││  0x00007fc4c18091e4: je     0x00007fc4c18091d0
 14.88%   20.52%     ││  0x00007fc4c18091e6: mov    0x8(%rax),%r11d
 15.11%   19.60%     ││  0x00007fc4c18091ea: shl    $0x3,%r11
 10.65%   11.80%     ││  0x00007fc4c18091ee: cmp    %r8,%r11
  0.01%              ╰│  0x00007fc4c18091f1: je     0x00007fc4c18091d0
                      │  0x00007fc4c18091f3: cmp    (%r11,%rcx,1),%r8
                      ╰  0x00007fc4c18091f7: je     0x00007fc4c18091d0
                         0x00007fc4c18091f9: cmp    $0x18,%ecx
                         0x00007fc4c18091fc: jne    0x00007fc4c1809223
                         0x00007fc4c1809202: push   %rax
                         0x00007fc4c1809203: mov    %r8,%rax
                         0x00007fc4c1809206: push   %rcx
                         0x00007fc4c1809207: push   %rdi
                         0x00007fc4c1809208: mov    0x20(%r11),%rdi
                         0x00007fc4c180920c: mov    (%rdi),%ecx
.............................................................................................
 75.96%   90.09%  <total for region 1>

实验结果表明,程序中最热点的部份在方法StubRoutines::checkcast_arraycopy中。我们稍后再详细分析,但主要问题是,我们的性能现象表现在生成代码(generated code)中,甚至可能不是java支持的生成代码。虽然本文的进一步推论可以通过perfasm来重现,但我想着眼于另一种更传统、可扩展高负载的方法。

Meet Solaris Studio Performance Analyzer

想象一下,我们需要一个没有偏见的Java/native代码可用的,硬件计数器可选的分析器?真的有一个这样的APP。它的名字容易让人误解,叫Solaris Studio Performance Analyzer,但它同样可用于Linux系统。

适合用于分析程序的workflow有好多,以下是我们在实验中最常用的。分析器由两个不同的部分组成:一个用于收集性能数据的收集器和一个处理分析结果的分析器GUI。用命令行收集工具是很方便的,因为你可以给这些常用的命令设置好用的别名。

$ tail ~/.bashrc
# Collect Java+native, clock profiling
alias 'perfanal'="collect -o test.1.er -S on -j on -A on "

# Collect native only (useful when loading *lots* of classes
# -- avoids profiler's JVMTI hooks overheads), clock profiling
alias 'perfanal-native'="collect -o test.1.er -S on -j off -A on "

# Collect Java+native, hardware counter profiling
alias 'perfanal-hwc'="collect -o test.1.er -S on -j on -A on -h cycles,on,insts,on "

# Collect native only, hardware counting profiling
alias 'perfanal-hwc-native'="collect -o test.1.er -S on -j off -A on -h cycles,on,insts,on "

这些别名让你可以快速地把分析器连接到JMH

$ perfanal-hwc java -jar benchmarks.jar -f 1 -p size=1000 -p type=arraylist

这个命令会在当前工作目录生成一个test.${n}.er的结果文件,你可以用分析器GUI打开它。


Figure 4. ArrayList (size = 1000), toArray().

simple中,我们最热点的代码是jint_disjoint_arraycopy,看起来这个方法实现的是非连续(disjoint)数组的"int" arraycopy,

注意:分析器是如何处理Java代码(org.openjdk...), 虚拟机native代码(ParallelTaskTerminator::.. 和SpinPause) ,和调用call_stub生成的汇编代码(e.g. jint_disjoint_arraycopy)的。在复杂的场景下,调用树会同时展示java代码和native代码,当我们调试JNI/JNA时非常有用。调用树把jint_disjoint_arraycopy当作一个叶子方法(leaf function)。这个方法没有其它的指示,但你可能通过Hotspot源码库搜索,找到stubRoutines.hpp。里面说"StubRoutines提供了编译代码和运行时系统所使用的装配例程的入口点。平台特定的入口点是在平台特定的内部类中定义的。", 编译阶段和运行阶段都使用它来优化一些操作,特别是arraycopy。

Figure 5. ArrayList (size = 1000), toArray(new T[list.size()]).

sized中,我们看到和simple不一样的arraycopy方法checkcast_arraycopy, 以及Java代码sized()方法。

Figure 6. ArrayList (size = 1000), toArray(new T[0]).

zero中,我们找到另一个类似的方法checkcast_arraycopy_uninit。看起来和checkcast_arraycopy差不多,但"uninit"是什么? 注意这里Java函数不是热点方法了。

这些方法的名称已经很直白了,但是在我们使用另一个厉害的分析器功能前这些都是猜测。当我们选中一个方法,然后点击"Disassembly"视图,我们看到..


Figure 7. simple case disassembly.

jint_disjoint_arraycopy使用AVX进行向量化拷贝!难怪这么快: 它大段地复制了内存。

Figure 8. zero case disassembly

zero disassembly(把高级代码编译成低级代码?)视图展示了非向量化的checkcast_arraycopy_uninit。它都做了什么?这个循环看起来和地址30~51拷贝的关,但看起来没有向量化。要理解这个方法做了什么需要更多的VM内部知识。通常,你可以通过连接java 代码来assembly(ps:把汇编反编译成高级代码?) ,但VM stubs和java代码没有关联。你也可以跟踪VM源码来生成stub。这里我们选择耐心地看代码。

循环通过movl(%rdi,%rdx,4),%eax读取,通过movl %eax, 0(%r13,%rdx,4)定入,%rdx在增加-- 这表明%rdx是循环计数器。把当前的内容读到%eax后,我们检查是否为null,然后从偏移0x8加载一些内容, shift it, 然后和其它东西做比较。这就是加载一个class word,将其展开,并对其进行类型检查。

你可以通过JOL来查看java对象运行时的表示。看一下这个例子JOLSample_11_ClassWord example.

Figure 9. sized case disassembly, arraycopy stub

sized有两个热点,一个是checkcast_arraycopy ,和zero一样。但我们看到另一个热点:

Figure 10. sized case disassembly, Java code

大多数Cpu Cycles都花费在repz stosb。如果不看生成代码,这个方法比较难理解。prefetchnta在HotSpot-generated代码中通常是"allocation prefetch"(见command line docs,"-XX:Allocate..."), 和对象分配关联。事实上,我们在%r9有一个"新"对象地址,并放入了mark word,class word然后用repz stosb对存储空间清零--相当于memset to zero。分配本身是一个数组分配,我们看到的是数组归零。你可以在Why Nothing Matters: The Impact of Zeroing中看详细解析。

初步见解

综上,我们有了初步见解:

  1. simplesizedzero快主要因为使用的向量化数组复制比用类型检查数组复制快很多
  2. zerosized快是因为zero神奇地躲避array zeroing。而“反射”数组创建似乎根本不影响zero性能。

用HashSet重做实验这部分留给读者。结果应该基本和我们的初步见解一致。

后续行动

在很多情况下,继续跟进当前的发现来确定这个结论有多通用,你是否真的了解了背后的原因是非常有意义的。但要小心的是,这可能是个性能工程的兔子洞(兔子洞:指的是是很难搞的事情,一时半会解决不了的问题), 你永远到不了洞底,只能停止探索。

New Reflective Array

首先,让我们来解构"反射数组创建是很慢的"这个直觉。我们知道,很多Collection初始化一个给定类型数组时用的都是Array.newInstance(Class<?>,int)--我们先来试试这个。用同样的压测方法来设计实验,但只留下newInstance代码。

@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgsAppend = {"-XX:+UseParallelGC", "-Xms1g", "-Xmx1g"})
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class ReflectiveArrayBench {

    @Param({"0", "1", "10", "100", "1000"})
    int size;

    @Benchmark
    public Foo[] lang() {
        return new Foo[size];
    }

    @Benchmark
    public Foo[] reflect() {
        return (Foo[]) Array.newInstance(Foo.class, size);
    }
}

现在看下实验结果会发现,"language"和反射调用的性能几乎一样。

Benchmark                     (size)  Mode  Cnt    Score    Error  Units

# default
ReflectiveArrayBench.lang          0  avgt    5   17.065 ±  0.224  ns/op
ReflectiveArrayBench.lang          1  avgt    5   12.372 ±  0.112  ns/op
ReflectiveArrayBench.lang         10  avgt    5   14.910 ±  0.850  ns/op
ReflectiveArrayBench.lang        100  avgt    5   42.942 ±  3.666  ns/op
ReflectiveArrayBench.lang       1000  avgt    5  267.889 ± 15.719  ns/op

# default
ReflectiveArrayBench.reflect       0  avgt    5   17.010 ±  0.299  ns/op
ReflectiveArrayBench.reflect       1  avgt    5   12.542 ±  0.322  ns/op
ReflectiveArrayBench.reflect      10  avgt    5   12.835 ±  0.587  ns/op
ReflectiveArrayBench.reflect     100  avgt    5   42.691 ±  2.204  ns/op
ReflectiveArrayBench.reflect    1000  avgt    5  264.408 ± 22.079  ns/op

为什么会这样?我昂贵的反射调用去哪里了?要想长话短说,你通常可以通过这些步骤来把这种“古怪”的现象一分为二。首先,google/记住很长一段时间Reflection调用会被inflated(当某个地本地方法调用次数超出阈值时,会从昂贵的JNI调用膨胀为与JNI无关的生成代码的调用)。关闭反射膨胀后,实现结果如下:

Benchmark                     (size)  Mode  Cnt    Score   Error  Units

# -Dsun.reflect.inflationThreshold=2147483647
ReflectiveArrayBench.reflect       0  avgt   10   17.253 ± 0.470  ns/op
ReflectiveArrayBench.reflect       1  avgt   10   12.418 ± 0.101  ns/op
ReflectiveArrayBench.reflect      10  avgt   10   12.554 ± 0.109  ns/op
ReflectiveArrayBench.reflect     100  avgt   10   39.969 ± 0.367  ns/op
ReflectiveArrayBench.reflect    1000  avgt   10  252.281 ± 2.630  ns/op

hm...不是这个原因。我们再看一下源码:
jdk/src/java.base/share/classes/java/lang/reflect/Array.java

class Array {
    public static Object newInstance(Class<?> componentType, int length)
        throws NegativeArraySizeException {
        return newArray(componentType, length);
    }

    @HotSpotIntrinsicCandidate // <--- 哦?这是个啥?
    private static native Object newArray(Class<?> componentType, int length)
        throws NegativeArraySizeException;
}

jdk/src/java.base/share/classes/jdk/internal/HotSpotIntrinsicCandidate.java

/**
 * The {@code @HotSpotIntrinsicCandidate} annotation is specific to the
 * HotSpot Virtual Machine. It indicates that an annotated method
 * may be (but is not guaranteed to be) intrinsified by the HotSpot VM. A method
 * is intrinsified if the HotSpot VM replaces the annotated method with hand-written
 * assembly and/or hand-written compiler IR -- a compiler intrinsic -- to improve
 * performance. The {@code @HotSpotIntrinsicCandidate} annotation is internal to the
 * Java libraries and is therefore not supposed to have any relevance for application
 * code.
 */
public @interface HotSpotIntrinsicCandidate { ... }

(注释中的意思大概直接调用 JVM 内部实现(可能是手写的汇编代码或中间代码),不走常规 JNI lookup,来提高性能。)

所以,Array.newArray是VM自己才知道的方法。有意思,我们来看下VM中的实现:
hotspot/src/share/vm/classfile/vmSymbols.hpp

  do_intrinsic(_newArray, java_lang_reflect_Array, newArray_name, newArray_signature, F_SN) \
   do_name(     newArray_name,       "newArray")                                            \
   do_signature(newArray_signature,  "(Ljava/lang/Class;I)Ljava/lang/Object;")              \

Instrinsic?让我把你关了吧:

# -XX:+UnlockDiagnosticVMOptions -XX:DisableIntrinsic=_newArray
ReflectiveArrayBench.reflect       0  avgt    5   67.594 ± 4.795  ns/op
ReflectiveArrayBench.reflect       1  avgt    5   69.935 ± 7.766  ns/op
ReflectiveArrayBench.reflect      10  avgt    5   73.588 ± 0.329  ns/op
ReflectiveArrayBench.reflect     100  avgt    5   86.598 ± 1.735  ns/op
ReflectiveArrayBench.reflect    1000  avgt    5  409.786 ± 9.148  ns/op

啊哈!找到你了!toArray方法被VM特殊处理了,并且产生了的相同的生成代码。但先别根据你在网上看到的解释来猜测它到底干了什么。我们先来看一下性能分析器的分析结果:


Figure 11. _newArray intrinsic enabled
Figure 12. _newArray intrinsic disabled

有什么区别?当intrinsic关闭后,我们看到Java方法Array.newArray调用JVM_NewArray本地方法,再调用VM的Reflection::...,再然后向GC申请空间来分配数组。这就是人们曾经见过的昂贵的"Array.newArray"调用了。

但是,这个昂贵的调用在 JDK-6525802就修复了。我们稍后会回归几个历史版本的jdk看看。

Empty Array Instantiation

现在,让我们将注意力转移到数组实例化成本上。在我们跳到零化消去之前,理解分配本身是如何工作的是很重要的。事后来看,我们想要测量恒定大小的数组和非恒定大小的数组(这些数组的大小并不是静态的)。编译器能基于这些已知条件做点什么吗?

先来看看,我们构建了一个这样的JMH基准测试(为了让它看起来短一点,我用伪代码来描述)

@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgsAppend = {"-XX:+UseParallelGC", "-Xms1g", "-Xmx1g"})
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class EmptyArrayBench {

#for L in 0..512
    int v$L = $L;

    @Benchmark
    public Foo[] field_$L() {
        return new Foo[v$L];
    }

    @Benchmark
    public Foo[] const_$L() {
        return new Foo[$L];
    }
#done

结果如下:

Benchmark                 Mode  Cnt   Score   Error  Units

# -----------------------------------------------------------

EmptyArrayBench.const_000  avgt   15    2.847 ± 0.016  ns/op
EmptyArrayBench.const_001  avgt   15    3.090 ± 0.020  ns/op
EmptyArrayBench.const_002  avgt   15    3.083 ± 0.022  ns/op
EmptyArrayBench.const_004  avgt   15    3.336 ± 0.029  ns/op
EmptyArrayBench.const_008  avgt   15    4.618 ± 0.047  ns/op
EmptyArrayBench.const_016  avgt   15    7.568 ± 0.061  ns/op
EmptyArrayBench.const_032  avgt   15   13.935 ± 0.098  ns/op
EmptyArrayBench.const_064  avgt   15   25.905 ± 0.183  ns/op
EmptyArrayBench.const_128  avgt   15   52.807 ± 0.252  ns/op
EmptyArrayBench.const_256  avgt   15  110.208 ± 1.006  ns/op
EmptyArrayBench.const_512  avgt   15  171.864 ± 0.777  ns/op

# -----------------------------------------------------------

EmptyArrayBench.field_000  avgt   15   16.998 ± 0.063  ns/op
EmptyArrayBench.field_001  avgt   15   12.400 ± 0.065  ns/op
EmptyArrayBench.field_002  avgt   15   12.651 ± 0.332  ns/op
EmptyArrayBench.field_004  avgt   15   12.434 ± 0.062  ns/op
EmptyArrayBench.field_008  avgt   15   12.504 ± 0.049  ns/op
EmptyArrayBench.field_016  avgt   15   12.588 ± 0.065  ns/op
EmptyArrayBench.field_032  avgt   15   14.423 ± 0.121  ns/op
EmptyArrayBench.field_064  avgt   15   26.145 ± 0.166  ns/op
EmptyArrayBench.field_128  avgt   15   53.092 ± 0.291  ns/op
EmptyArrayBench.field_256  avgt   15  110.275 ± 1.304  ns/op
EmptyArrayBench.field_512  avgt   15  174.326 ± 1.642  ns/op

# -----------------------------------------------------------

Oops.看起来field_*在小数组长度的实验中输了。这是为什么呢? -prof perfasm输出结果如下:

Hottest allocation code in const_0008:

  2.30%    1.07%  0x00007f32cd1f9f76: prefetchnta 0xc0(%r10)
  3.34%    3.88%  0x00007f32cd1f9f7e: movq   $0x1,(%rax)
  3.66%    4.39%  0x00007f32cd1f9f85: prefetchnta 0x100(%r10)
  1.63%    1.91%  0x00007f32cd1f9f8d: movl   $0x20018fbd,0x8(%rax)
  1.76%    2.31%  0x00007f32cd1f9f94: prefetchnta 0x140(%r10)
  1.52%    2.14%  0x00007f32cd1f9f9c: movl   $0x8,0xc(%rax)
  2.77%    3.67%  0x00007f32cd1f9fa3: prefetchnta 0x180(%r10)
  1.77%    1.80%  0x00007f32cd1f9fab: mov    %r12,0x10(%rax)
  4.40%    4.61%  0x00007f32cd1f9faf: mov    %r12,0x18(%rax)
  4.64%    3.97%  0x00007f32cd1f9fb3: mov    %r12,0x20(%rax)
  4.83%    4.49%  0x00007f32cd1f9fb7: mov    %r12,0x28(%rax)
  2.03%    2.71%  0x00007f32cd1f9fbb: mov    %r8,0x18(%rsp)
  1.35%    1.25%  0x00007f32cd1f9fc0: mov    %rax,%rdx

Hottest allocation code in field_0008:

 0.02%           0x00007f27551fb424: prefetchnta 0xc0(%r11)
  5.53%    7.55%  0x00007f27551fb42c: movq   $0x1,(%r9)
  0.02%           0x00007f27551fb433: prefetchnta 0x100(%r11)
  0.05%    0.06%  0x00007f27551fb43b: movl   $0x20018fbd,0x8(%r9)
           0.01%  0x00007f27551fb443: mov    %edx,0xc(%r9)
  2.03%    1.78%  0x00007f27551fb447: prefetchnta 0x140(%r11)
  0.04%    0.07%  0x00007f27551fb44f: mov    %r9,%rdi
  0.02%    0.02%  0x00007f27551fb452: add    $0x10,%rdi
  0.02%    0.01%  0x00007f27551fb456: prefetchnta 0x180(%r11)
  1.96%    1.05%  0x00007f27551fb45e: shr    $0x3,%rcx
  0.02%    0.02%  0x00007f27551fb462: add    $0xfffffffffffffffe,%rcx
  0.01%    0.03%  0x00007f27551fb466: xor    %rax,%rax
  0.01%           0x00007f27551fb469: shl    $0x3,%rcx
  1.96%    0.06%  0x00007f27551fb46d: rep rex.W stos %al,%es:(%rdi) ; <--- huh...
 39.44%   78.39%  0x00007f27551fb470: mov    %r8,0x18(%rsp)
  8.01%    0.18%  0x00007f27551fb475: mov    %r9,%rdx

进一步分析得到,const_*在达到一定阈值之后会切换到rep stos(和大多数memset的实现差不多),试图躲过rep stos的设置成本。但field_*的例子中,不知道静态长度,因此总是会执行rep stos,并且在小长度数组情况下影响明显。

这是可以修复的,见 JDK-8146801。但是,固定长度的数组性能总是更好一些。你知道这是怎么应用到toArray情况上的吗?在较小的集合大小时,比较new T[0]或new T[coll.size()]。这就回答了zero在小集合上的“异常”优势。

这又是另一个“编译器能猜测到越多信息,优化效果就越好”的例子。一个更有趣的例子在文章Faster Atomic*FieldUpdaters for Everyone描述。

Uninitialized Arrays

现在,轮到讲zeroing的故事了。Java语言规范里要求,每一个新的数组或对象的准备阶段都需要有初始值(zeros)。因此,在运行阶段必需zero-out这些分配了的内存。但是,如果在对象/数组对其它对象可见之前用一些初始值覆盖该存储,我们就可以将初始化和zeroing合并,从而高效地消除了零值。注意,"可见"在面对异常返回,finalizers和其它VM内部成员时很难定义, 详见: e.g. JDK-8074566

我们看到zero的例子中躲避了zeroing过程,但sized中却没有,这是为什么?为了找到原因,我们需要评估一下不同的代码中分配数组是怎么实现的。

@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgsAppend = {"-XX:+UseParallelGC", "-Xms1g", "-Xmx1g"})
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class ArrayZeroingBench {
    @Param({"1", "10", "100", "1000"})
    int size;

    Object[] src;

    @Setup
    public void setup() {
        src = new Object[size];
        for (int c = 0; c < size; c++) {
            src[c] = new Foo();
        }
    }

    @Benchmark
    public Foo[] arraycopy_base() {
        Object[] src = this.src;
        Foo[] dst = new Foo[size];
        System.arraycopy(src, 0, dst, 0, size - 1);
        return dst;
    }

    @Benchmark
    public Foo[] arraycopy_field() {
        Object[] src = this.src;
        Foo[] dst = new Foo[size];
        System.arraycopy(src, 0, dst, 0, size);
        return dst;
    }

    @Benchmark
    public Foo[] arraycopy_srcLength() {
        Object[] src = this.src;
        Foo[] dst = new Foo[size];
        System.arraycopy(src, 0, dst, 0, src.length);
        return dst;
    }

    @Benchmark
    public Foo[] arraycopy_dstLength() {
        Object[] src = this.src;
        Foo[] dst = new Foo[size];
        System.arraycopy(src, 0, dst, 0, dst.length);
        return dst;
    }

    @Benchmark
    public Foo[] copyOf_field() {
        return Arrays.copyOf(src, size, Foo[].class);
    }

    @Benchmark
    public Foo[] copyOf_srcLength() {
        return Arrays.copyOf(src, src.length, Foo[].class);
    }

    public static class Foo {}
}

实验结果如下:

Benchmark                (size)  Mode  Cnt    Score    Error  Units
AZB.arraycopy_base            1  avgt   15   14.509 ±  0.066  ns/op
AZB.arraycopy_base           10  avgt   15   23.676 ±  0.557  ns/op
AZB.arraycopy_base          100  avgt   15   92.557 ±  0.920  ns/op
AZB.arraycopy_base         1000  avgt   15  899.859 ±  7.303  ns/op

AZB.arraycopy_dstLength       1  avgt   15   17.929 ±  0.069  ns/op
AZB.arraycopy_dstLength      10  avgt   15   23.613 ±  0.368  ns/op
AZB.arraycopy_dstLength     100  avgt   15   92.553 ±  0.432  ns/op
AZB.arraycopy_dstLength    1000  avgt   15  902.176 ±  5.816  ns/op

AZB.arraycopy_field           1  avgt   15   18.063 ±  0.375  ns/op
AZB.arraycopy_field          10  avgt   15   23.443 ±  0.278  ns/op
AZB.arraycopy_field         100  avgt   15   93.207 ±  1.565  ns/op
AZB.arraycopy_field        1000  avgt   15  908.663 ± 18.383  ns/op

AZB.arraycopy_srcLength       1  avgt   15    8.658 ±  0.058  ns/op
AZB.arraycopy_srcLength      10  avgt   15   14.114 ±  0.084  ns/op
AZB.arraycopy_srcLength     100  avgt   15   79.778 ±  0.639  ns/op
AZB.arraycopy_srcLength    1000  avgt   15  681.040 ±  9.536  ns/op

AZB.copyOf_field              1  avgt   15    9.383 ±  0.053  ns/op
AZB.copyOf_field             10  avgt   15   14.729 ±  0.091  ns/op
AZB.copyOf_field            100  avgt   15   81.198 ±  0.477  ns/op
AZB.copyOf_field           1000  avgt   15  671.670 ±  6.723  ns/op

AZB.copyOf_srcLength          1  avgt   15    8.150 ±  0.409  ns/op
AZB.copyOf_srcLength         10  avgt   15   13.214 ±  0.112  ns/op
AZB.copyOf_srcLength        100  avgt   15   80.718 ±  1.583  ns/op
AZB.copyOf_srcLength       1000  avgt   15  671.716 ±  5.499  ns/op

当然,如果你想学习生成代码来解析你对以上结果的任何猜想,我们已经帮你做了,见JDK-8146828。VM尝试消除尽可能多的零值,因为这个优化是相当有用的--这就是为什么zero(使用类似copyOf_*)得到了好处的原因了。

但也有例外的情况,在一些情况下,零值没有被有效消除,这是需要被修复的。详见JDK-8146828

Caching the Array

有人会问:给出的zero例子已经被优化得很好了,我们还能进一步地把这个大小为零的数组缓存为静态变量以得到更高的性能吗?这样我们就可以完全避免 stray allocation了?我们来看一看这个zero_cached实验:

@State(Scope.Benchmark)
public class ToArrayBench {

    // Note this is *both* static *and* final
    private static final Foo[] EMPTY_FOO = new Foo[0];

    ...

    @Benchmark
    public Foo[] zero() {
        return coll.toArray(new Foo[0]);
    }

    @Benchmark
    public Foo[] zero_cached() {
        return coll.toArray(EMPTY_FOO);
    }
}

注意这两个例子稍有区别:对于空集合,zero_cached会返回同一个数组,而不是每次调用都会生成一个新数组。如果用户使用数组标识进行某些操作,这可能会有问题。

实验结果如下:

Benchmark               (size)     (type)  Mode  Cnt     Score    Error  Units

# ----------------------------------------------------------------------------

ToArrayBench.zero            0  arraylist  avgt   15     4.352 ±  0.034  ns/op
ToArrayBench.zero            1  arraylist  avgt   15    10.574 ±  0.075  ns/op
ToArrayBench.zero           10  arraylist  avgt   15    15.965 ±  0.166  ns/op
ToArrayBench.zero          100  arraylist  avgt   15    81.729 ±  0.650  ns/op
ToArrayBench.zero         1000  arraylist  avgt   15   685.616 ±  6.637  ns/op

ToArrayBench.zero_cached     0  arraylist  avgt   15     4.031 ±  0.018  ns/op
ToArrayBench.zero_cached     1  arraylist  avgt   15    10.237 ±  0.104  ns/op
ToArrayBench.zero_cached    10  arraylist  avgt   15    15.401 ±  0.903  ns/op
ToArrayBench.zero_cached   100  arraylist  avgt   15    82.643 ±  1.040  ns/op
ToArrayBench.zero_cached  1000  arraylist  avgt   15   688.412 ± 18.273  ns/op

# ----------------------------------------------------------------------------

ToArrayBench.zero            0    hashset  avgt   15     4.382 ±  0.028  ns/op
ToArrayBench.zero            1    hashset  avgt   15    23.877 ±  0.139  ns/op
ToArrayBench.zero           10    hashset  avgt   15    44.172 ±  0.353  ns/op
ToArrayBench.zero          100    hashset  avgt   15   282.852 ±  1.372  ns/op
ToArrayBench.zero         1000    hashset  avgt   15  4370.370 ± 64.018  ns/op

ToArrayBench.zero_cached     0    hashset  avgt   15     3.525 ±  0.005  ns/op
ToArrayBench.zero_cached     1    hashset  avgt   15    23.791 ±  0.162  ns/op
ToArrayBench.zero_cached    10    hashset  avgt   15    44.128 ±  0.203  ns/op
ToArrayBench.zero_cached   100    hashset  avgt   15   282.052 ±  1.469  ns/op
ToArrayBench.zero_cached  1000    hashset  avgt   15  4329.551 ± 36.858  ns/op

# ----------------------------------------------------------------------------

和想像中一样,这个优化只影响了小长度数组的情况,而且差距并不明显。这种改进并不能证明我们在大数组长度的情况下也是合理的。作为一个微小的优化,它可能在一些紧凑的代码中是有意义的,但我并不关心其他。

Historical Perspective

现在,我们跟踪jdk历史版本来看看,之前的基准测试在不同的jdk版本上表现怎么样。JMH基准测试可以在JDK 6及以上版本使用。让我们看看JDK 6生命周期中的几个有趣点,并看看最新的JDK8u66和JDK9b99的性能。我们不需要测试所有数组长度,只要考虑一些合理的长度

enchmark            (size)     (type)  Mode  Cnt    Score   Error  Units

# --------------------------------------------------------------------------

# 6u6 (2008-04-16)
ToArrayBench.simple     100  arraylist  avgt   30  122.228 ± 1.413  ns/op
ToArrayBench.sized      100  arraylist  avgt   30  139.403 ± 1.024  ns/op
ToArrayBench.zero       100  arraylist  avgt   30  155.176 ± 3.673  ns/op

# 6u12 (2008-12-12)
ToArrayBench.simple     100  arraylist  avgt   30   84.760 ± 1.283  ns/op
ToArrayBench.sized      100  arraylist  avgt   30  142.400 ± 2.696  ns/op
ToArrayBench.zero       100  arraylist  avgt   30   94.132 ± 0.636  ns/op

# 8u66 (2015-11-16)
ToArrayBench.simple     100  arraylist  avgt   30   41.174 ± 0.953  ns/op
ToArrayBench.sized      100  arraylist  avgt   30   93.159 ± 0.368  ns/op
ToArrayBench.zero       100  arraylist  avgt   30   80.193 ± 0.362  ns/op

# 9b99 (2016-01-05)
ToArrayBench.simple     100  arraylist  avgt   30   40.874 ± 0.352  ns/op
ToArrayBench.sized      100  arraylist  avgt   30   93.170 ± 0.379  ns/op
ToArrayBench.zero       100  arraylist  avgt   30   80.966 ± 0.347  ns/op

# --------------------------------------------------------------------------

# 6u12 (2008-12-12)
ToArrayBench.simple     100    hashset  avgt   30  585.766 ± 5.946  ns/op
ToArrayBench.sized      100    hashset  avgt   30  670.119 ± 0.959  ns/op
ToArrayBench.zero       100    hashset  avgt   30  745.802 ± 5.309  ns/op

# 6u16 (2009-08-11)
ToArrayBench.simple     100    hashset  avgt   30  561.724 ± 5.094  ns/op
ToArrayBench.sized      100    hashset  avgt   30  634.155 ± 0.557  ns/op
ToArrayBench.zero       100    hashset  avgt   30  634.300 ± 1.206  ns/op

# 6u21 (2010-07-07)
ToArrayBench.simple     100    hashset  avgt   30  565.139 ± 3.763  ns/op
ToArrayBench.sized      100    hashset  avgt   30  623.901 ± 4.027  ns/op
ToArrayBench.zero       100    hashset  avgt   30  605.833 ± 2.909  ns/op

# 8u66 (2015-11-16)
ToArrayBench.simple     100    hashset  avgt   30  297.281 ± 1.258  ns/op
ToArrayBench.sized      100    hashset  avgt   30  387.633 ± 0.787  ns/op
ToArrayBench.zero       100    hashset  avgt   30  307.410 ± 6.981  ns/op

# 9b99 (2016-01-05)
ToArrayBench.simple     100    hashset  avgt   30  298.032 ± 3.925  ns/op
ToArrayBench.sized      100    hashset  avgt   30  316.250 ± 9.614  ns/op
ToArrayBench.zero       100    hashset  avgt   30  284.431 ± 6.201  ns/op

# --------------------------------------------------------------------------

所以,zero至少5年前就比sized快了。

结论

综上所述,我们可以将我们的初步见解推广到实际结论:

  1. 反射数组创建根本不影响toArray(newT[0])的性能,因为当前VM已经很好地优化了Arrays.newArray的实现。因此,PMD和IDEA静态分析规则的前提已经失效了。这是一方面影响,剩余的影响来自于不同的toArray方法实现细节差异。
  2. Object[] toArray()中的向量数组复制远比toArray(new T[size])中的类型检查数据复制快。但是,如果我们想要用T[],这个因素并不影响。
  3. 造成toArray(new T[size])和toArray(new T[0])在小数组上的表现差异的重要原因是预测数组大小能力。可能因为数组大小作为常数被加载,而size()需要调用collection自己获取,我们需要寄望于JIT成功内联这个方法并且size()的时间复杂度是O(1)。更糟的是,如果集合大小在轮询size()和调用toArray()之间发生了更改,那么我们就惨了。
  4. 造成toArray(new T[size])和toArray(new T[0])间的差异的更重要原因是zeroing elimination, 这是基于VM发现一个新分配的数组已经完全填充的能力。目前的实验表明,至少在ArrayList情况下,内部分配的数组因此更快,并且外部分配的数组有可能在相同的机制上重新分配。如果我们能完全消除zeroing,也就不用担心上一个警告了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,192评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,186评论 1 303
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,844评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,471评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,876评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,891评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,068评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,791评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,539评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,772评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,250评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,577评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,244评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,146评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,949评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,995评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,812评论 2 276

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,296评论 18 399
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,795评论 2 89
  • ArrayList 源码分析 不知道各位朋友,还记得开工前制定的学习目标么? 有没有一直为了那个目标废寝忘食呢?继...
    醒着的码者阅读 1,330评论 5 11
  • 1 DAO-DCO-DCOT-TEOP 2 DCO,即新型的分布式协作组织:Distributed Collabo...
    高度基金Bennett阅读 779评论 0 2
  • 〔又是一篇很久很久之前寫的文~最近總在“翻舊賬”,嘿嘿〕 “其实任何东西,只要够深,都是一把刀。”过去总是要翻的,...
    FM812阅读 656评论 2 1