【问题标题】:why adding first item into collection much slower than second?为什么将第一项添加到集合中比第二项慢得多?
【发布时间】:2022-01-04 15:36:41
【问题描述】:

我在将第一个和第二个项目添加到集合中时遇到了巨大的性能差异(尝试过 ArrayListHashSet),但我无法解释原因。已搜索但未找到任何答案。

public class Main {

    public static void main(String[] args) {
        // also tried HashSet
        // also tried new ArrayList<>(2)
        ArrayList<String> collection = new ArrayList<>();
        long t1 = System.nanoTime();
        collection.add("a");
        long t2 = System.nanoTime();
        collection.add("b");
        long t3 = System.nanoTime();
        System.out.println(String.valueOf(t2 - t1) + "\n"
                + String.valueOf(t3 - t2));
        //typical output:
        //4399
        //1201
    }
}

一些猜测:

  • 因为添加第一项时集合被延迟初始化?
  • 还是我使用了错误的方法来衡量性能?
  • 或与 jvm 的工作方式有关(我不知道)?

环境:jdk11、win10、intellij。

【问题讨论】:

  • 可能有很多原因。一般来说,这不是一个好的基准,JVM 必须预热、加载类和编译代码。这可能会以某种未指定的顺序发生。
  • 差异为 3µs,即使在硬实时环境中也肯定不明显。要获得更相关的结果,请创建一个执行时间为几秒的测试程序,然后查看第一次和第二次添加之间是否仍然存在显着差异(可能仍然不是有效的基准,但比当前测试要好得多)。然后使用分析器查看程序的哪一部分消耗了 CPU 时间。

标签: java performance arraylist collections timing


【解决方案1】:

你的三个猜测都是正确的:)

你问了一个很好的问题。请注意:微基准非常危险,您可能很容易对代码部分的速度做出完全错误的假设。你对延迟初始化是正确的,但它是相当幸运的:)

最重要的是你的猜测是正确的,你“使用了错误的方法来衡量性能”。

您无法像这样衡量 Java 应用程序的速度。这根本不可能。该过程过于随机且过于依赖许多其他因素,尤其是运行时的 JIT(即时优化)。

试试看,例如这里:https://www.baeldung.com/java-microbenchmark-harness,尝试运行它并思考它是如何工作的。

TL;DR:JVM 必须首先“预热”,然后您必须多次运行测试代码并计算平均时间。仍然可能有很多优化导致某些代码根本没有执行:)

如果您不想玩微基准库,至少将您的代码移动到一个方法中并调用该方法 20 次。我就是这么做的(只是把\n换成了---)结果是这样的:

10800---1400
1500---200
600---100
500---100
700---100
400---100
400---100
400---100
400---100
500---100
400---100
400---100
400---100
400---100
300---100
400---100
300---100
500---100
400---100
300---100

如您所见,热身是最重要的因素。然而延迟初始化的影响也是可见的。

【讨论】:

  • OP 不问如何衡量 Java 应用程序的“速度”,而是问他得到的结果为何如此。无论是“太随机”,结果始终以至少 2:1 的比例存在差异。
  • @starikoff 但是测量速度的方法对于这个问题至关重要!根据 OP 提供的方法,您甚至无法肯定地说“添加第 1 项和第 2 项的时间不同”!如果您以错误的假设开始一个问题,您必须先修正该假设,然后才能继续。 OP 在他的三个猜测中是正确的:)
  • 您看时钟 3 次并记下时间差。第一个比第二个大 2-6 倍。你做了100次,你总是这样。为什么问为什么不合法?
  • @starikoff 问为什么确实是合法的!我只是想警告 OP,这个问题主要是基于错误的假设。而他自己也猜对了,他“用了错误的方式来衡量绩效”!在这里,OP 对延迟初始化问题也是正确的,但是他很幸运!微基准非常危险,您很容易对代码的速度做出完全错误的假设。
  • @starikoff 看看我的编辑,希望我现在能更清楚地表达自己。
【解决方案2】:

这是因为 Of 延迟初始化。当你运行这条线时

ArrayList<String> collection = new ArrayList<>();

它只持有对列表的引用,但该列表不会发生实际的内存分配。但是,当您将第一个元素添加到集合中时,它首先为列表的下 10 个元素分配内存(10 是数组列表的默认大小),然后添加第一个值。 接下来的 9 个元素的结果插入所需的时间更少,但再次插入第 11 个元素的结果将比以前花费更多的时间。

    public static void main(String[] args) {

    ArrayList<String> collection = new ArrayList<>();
    
    for (int i = 0; i < 12; i++) {
          long t1 = System.nanoTime();
          collection.add("a");
          long t2 = System.nanoTime();
          System.out.println("Index : "+ (i+1) +": Time: "+ String.valueOf(t2 - t1));
    }
    /** Output:
     *  Index : 1: Time: 6800
        Index : 2: Time: 800
        Index : 3: Time: 500
        Index : 4: Time: 700
        Index : 5: Time: 600
        Index : 6: Time: 500
        Index : 7: Time: 600
        Index : 8: Time: 600
        Index : 9: Time: 500
        Index : 10: Time: 500
        Index : 11: Time: 2800
        Index : 12: Time: 500
     */

}

【讨论】:

  • 即使您对 ArrayList 类的延迟初始化是正确的,您也不能像这样运行微基准测试。代码本质上是错误的。你的方式有 JIT,而且 I/O 也很昂贵,我怀疑第一个 System.out.println 的成本会与第一个操作的低速有关。
  • ArrayList 的 OpenJDK11 实现的引用确认了在调用 add(E e) 时它是延迟分配的,即使在默认情况下大小应该为 10。但完全同意基准 cmets,文档说The add operation runs in **amortized** constant time是有原因的。
  • @Avinash 是的,集合是延迟初始化的,但是不,您的代码没有“证明”它。提示:用new ArrayList&lt;&gt;(100) 代替new ArrayList&lt;&gt;() 试试你的代码:第一次迭代仍然比第二次大几倍。
  • @AlexeyVeleshko 是的,你是对的,时间之间没有 I/O,这是一个有效的点。如何完全禁用 JIT - 请参阅 stackoverflow.com/q/34315960/2886891。如何消除 JIT - 阅读我的答案并研究提供的链接。
  • @shriakhilc 在默认情况下,延迟分配不会“偶数”发生,而是发生。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-08
  • 2020-12-03
  • 2012-03-03
  • 1970-01-01
  • 2018-07-22
相关资源
最近更新 更多