【问题标题】:Why is Collection's .addAll slower than manually adding?为什么 Collection 的 .addAll 比手动添加慢?
【发布时间】:2014-03-09 17:56:19
【问题描述】:

我运行了两个测试用例(多次),似乎迭代地将值添加到我的列表中比使用 addAll 更快

String[] rawArgs = new String[]{"one", "two", "three", "four", "five"};

// More efficient - 894 ns
List<String> list = new ArrayList<>();
for (String s : rawArgs) {
    list.add(s);
}


// Less efficient - 1340 ns
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList(rawArgs));

我通过我的 IDE 以及其他人得到注释,后一种方式是将数组转换为该数据结构的“正确”方式。但如果它实际上比第一个慢,有什么优势(一些模糊的类型安全性?),我应该出于什么原因使用第二个?

编辑 - 代码基准测试:

JVM预热,先重新创建主类对象:

public static void main(String[] args) {
    Internet test;
    for (int i = 0; i < 15; i++) {
        test = new Internet(); // JVM warmup
    }
    test = new Internet();
    test.printOutput();
}

我只是在操作的两端取系统nanotime:

start = System.nanoTime();
/* function */
end = System.nanoTime();
result = end - start;

在测试用例中,每个开始/结束都有单独的字段,并在运行后计算结果(JVM也在运行测试之前通过循环实例来抢先预热)。

编辑 2 - 用更大的集合进行基准测试

经过一些测试(使用 Integer 代替,而不是手写所有数字),看起来更大的集合确实更慢:

100 个号码:

First operation: 18759ns
Second operation: 2680ns
Total operation: 21439ns

【问题讨论】:

  • 您如何对代码进行基准测试?
  • 会不会是Arrays.asList返回的额外List对象的创建?
  • @RohitJain 我会更新问题
  • 好的,现在尝试使用 100 个元素数组,然后使用 1000 个元素(微基准测试是万恶之源)。
  • @rgettman 这是一个潜在的想法,但Arrays 只是将数组包装在一个不可变的静态(假)ArrayList 类中,它不会构造一个全新的列表。

标签: java arrays collections


【解决方案1】:

for-each 循环解析为等价于

for (int i = 0; i < rawArgs.length; i++) {
  list.add(rawArgs[i]);
}

...而ArrayList.addAll 的实现实际上调用了toArray(),所以它最终调用了Arrays.asList(rawArgs).toArray(),这会产生冗余副本。也就是说,它还执行了System.arraycopy,这可能最终使它比 for 循环更快——它可以采用任何一种方式,并且根据其他一些基准,它实际上可能在不同的上下文中会有所不同。

Collections.addAll(Collection&lt;E&gt;, E...) 静态方法实际上旨在解决这个特定问题,并且比 addAll(Arrays.asList)) 更快,正如其 Javadoc 中明确指出的那样。

【讨论】:

  • 你看过Arrays.asList()的src代码吗,没有迭代器。它调用 java.util.Arrays.ArrayList 的构造函数(不是 java.util.ArrayList)
  • @AlexWien: Arrays.asListAbstractList 继承iterator() 方法,确实被调用了。
  • @AlexWien 该列表实现了 Iterable。如果它不使用迭代器,它在做什么?
  • @AlexWien:实际上,你在某种程度上是对的,尽管不是你想要的。 (ArrayList.addAll 调用 toArray(),而不是 iterator()。)现在已修复。
  • 谢谢。元素的数量似乎也令人怀疑,但Collections.addAll 也很不错!
【解决方案2】:

我尝试用大量的迭代重复这个实验,结果给你不同的结果:

public static void main(String[] args) {
    String[] rawArgs = new String[]{"one", "two", "three", "four", "five"};

    long start = System.nanoTime();
    for (int i = 0; i < 10000000; i++) {
        List<String> list = new ArrayList<>();
        for (String s : rawArgs) {
            list.add(s);
        }
    }
    long end = System.nanoTime();
    System.out.println("add():    " + (end - start));

    start = System.nanoTime();
    for (int i = 0; i < 10000000; i++) {
        List<String> list = new ArrayList<>();
        list.addAll(Arrays.asList(rawArgs));
    }
    end = System.nanoTime();
    System.out.println("addAll(): " + (end - start));

}

结果:

add():    310726674
addAll(): 233785566

具体数字各不相同,但addAll 在我的 JVM(在 Windows 上运行的 Sun JDK 1.7)上总是更快。两者的运行顺序也没有区别(我尝试了两种方式)所以这与热身无关。如果增加元素的数量,结果会更加显着,可能是因为必须调整 ArrayList 后面的数组的大小(因此需要额外的 arraycopy)。

【讨论】:

  • 可能是因为你运行了 10000000 次,而 OP 似乎只运行了一次。
  • 我添加了我使用的预热,但似乎元素数量可能是可疑情况
【解决方案3】:

这是一个基准,数组中的元素略多。结果清楚地表明addAll 方法以优势获胜:

public static void main(String[] args) {

    String[] rawArgs = new String[]{"one", "two", "three", "four", "five",
            "one", "two", "three", "four", "five",
            "one", "two", "three", "four", "five",
            "one", "two", "three", "four", "five",
            "one", "two", "three", "four", "five",
            "one", "two", "three", "four", "five",
            "one", "two", "three", "four", "five",
            "one", "two", "three", "four", "five"};

    /******** WARM UP JVM *********/

    for (int i = 0; i < 1000; ++i) {
        arrayToListLoop(rawArgs);
    }
    for (int i = 0; i < 1000; ++i) {
        arrayToListAddAll(rawArgs);
    }


    /** Actual measurement **/      
    long start = System.nanoTime();
    for (int i = 0; i < 1000; ++i) {
        arrayToListLoop(rawArgs);
    }
    long end = System.nanoTime();

    System.out.println((end - start) / 1000);

    start = System.nanoTime();
    for (int i = 0; i < 1000; ++i) {
        arrayToListAddAll(rawArgs);
    }
    end = System.nanoTime();

    System.out.println((end - start) / 1000);
}

public static void arrayToListLoop(String[] arr) {
    List<String> list = new ArrayList<>();
    for (String s : arr) {
        list.add(s);
    }
}

public static void arrayToListAddAll(String[] arr) {
    List<String> list = new ArrayList<>();
    list.addAll(Arrays.asList(arr));
}

结果:

1 首次运行:

2280
812

第二次运行:

1336
613

3 第三轮:

2088
751

【讨论】:

  • 我运行了这个并得到了相反的结果。 (Java 1.6/Hotspot 64 位服务器。)另外,如果我更改它以便将两个 ArrayList 都实例化为 new ArrayList&lt;String&gt;(arr.length),我会在 300, 2050 周围得到一些东西。虽然偶尔循环添加需要很长时间(~6000us),我想知道。
  • 我相信你对元素数量的想法是正确的,根据我在底部更新的 sn-p :)
  • @Radiodef 是的,如果你通过传递arr.length 参数来运行它,它会更快一些,因为在添加新元素的过程中不会创建任何额外的数组。我在 Java 8 64 位机器上运行了基准测试。
  • 我想知道有什么区别。我尝试延长预热时间,这确实缩短了 addAll 的时间,但循环仍然更快。
  • @Radiodef 可能他们在 Java 8 上更新了 addAll 方法。必须检查它们的源代码。
【解决方案4】:

我认为Collection.addAll() 更快,至少有两个原因:

ArrayList

  1. 它使用ensureCapacityInternal(size + numNew);,所以,如果添加的List很大,手动添加会被调用很多次,但在这种情况下会调用一次,有必要的容量。
  2. 它使用System.arraycopy(a, 0, elementData, size, numNew);方法进行复制,这是一种原生的高性能方法。

【讨论】:

  • 那么为什么要投反对票,请解释一下,如果您有更多相关意见认为我的解释不正确。
【解决方案5】:

时间基准有两种方法 + 正如其他人所指出的,您应该在进行实际计算之前预热 JVM。我个人从不决定基准时间何时小于 1 秒,我试图创造一个更大的问题来查看实际结果。所以,

如果您的时间太小(一些 ns 或 ms),那么您必须增加问题的大小,例如在您的情况下添加 N 个元素,例如 N>1000。

 int size = 10000;
 String[] rawArgs = new String[size];
 //add some elements for this test
 for (int i=0; i<size; i++) {
     rawArgs[i] = String.valueOf(i);
 }

要使用较小的大小进行测试,您始终必须预热 JVM(在实际测量之前进行一些测试测量)并创建一个循环来测量您的问题的多次运行,然后将总时间除以这​​些循环重复的次数.例如,

 //after the warm up try the following
 int repetitions = 1000;
 start = System.nanoTime();
 for (int i=0; i<repetitions; i++) {
     //your calculations
 }
 end = System.nanoTime();
 System.out.println("Cost per repetition: " + (end - start)/repetitions);

【讨论】:

    猜你喜欢
    • 2011-05-13
    • 1970-01-01
    • 2011-03-23
    • 2011-04-21
    • 2017-01-06
    • 2015-04-10
    • 1970-01-01
    • 2012-01-24
    • 1970-01-01
    相关资源
    最近更新 更多