0

I set out to test the following claim in the Javadoc for Collections::addAll:

The behavior of this convenience method is identical to that of c.addAll(Arrays.asList(elements)), but this method is likely to run significantly faster under most implementations.

I've used the following code to test the runtime of addAll for ArrayList and LinkedList (as well as Collections on the current collection):

public static void main(String[] args) {
  final String LIST_STRING = "ArrayList::addAll";
  Tracker arrTracker = new Tracker(LIST_STRING);
  Tracker colTracker = new Tracker("Collections::addAll");
  final String GREEN = "\u001B[32m";
  final String END_GREEN = "\u001B[0m";
  TimeMeasurer elapsed = new TimeMeasurer();
  Integer[] ints = new Integer[2_000_000];
  for(int i = 0; i < ints.length; i++)
  {
    ints[i] = Integer.valueOf((int) (Math.random()*50_001)-25_000);
  }
  System.out.println("Array filled! "+elapsed);
  List<Integer> collection = new ArrayList<>(List.of(1, 2, 3));
  List<Integer> list = new ArrayList<>(List.of(1, 2, 3));

  System.out.println("Created initial lists. "+elapsed);
  for(int i = 0; i < 4; ++i)
    Collections.addAll(collection,ints);
  System.out.println(GREEN+"Collections::addAll. "+elapsed+END_GREEN);
  collection.clear();
  collection = null;
  for(int i = 0; i < 5; i++)
    System.gc();
  System.out.println("Garbage collected. "+elapsed);
  for(int i = 0; i < 4; ++i)
    list.addAll(Arrays.asList(ints));
  System.out.println(GREEN+LIST_STRING+". "+elapsed+END_GREEN);
  list.clear();
  list = null;
  for(int i = 0; i < 5; i++)
    System.gc();
  System.out.println("Garbage collected. "+elapsed);

  //redoing
  for(int iter = 0; iter < 100; iter++)
  {
    System.out.println();
    collection = new ArrayList<>(List.of(1, 2, 3));
    list = new ArrayList<>(List.of(1, 2, 3));
    System.out.println("Recreated initial lists. "+elapsed);
    for(int i = 0; i < 4; ++i)
      Collections.addAll(collection,ints);
    System.out.println(GREEN+"Collections::addAll. "+colTracker.on(elapsed)+END_GREEN);
    collection.clear();
    collection = null;
    for(int i = 0; i < 5; i++)
      System.gc();
    System.out.println("Garbage collected. "+elapsed);
    for(int i = 0; i < 4; ++i)
      list.addAll(Arrays.asList(ints));
    System.out.println(GREEN+LIST_STRING+". "+arrTracker.on(elapsed)+END_GREEN);
    list.clear();
    list = null;
    for(int i = 0; i < 5; i++)
      System.gc();
    System.out.println("Garbage collected. "+elapsed);
  }
  System.out.println();
  System.out.println(colTracker.average());
  System.out.println(arrTracker.average());
}

I ignore the first run for the purposes of warming up the JVM, and change = new ArrayList<>(...) to = new LinkedList(...) when testing for LinkedList runtimes. Each addition consists of 2 million random integers, repeated 4 times, for a total of 8 million integers being appended. Here are the final outputs from the time testing code for LinkedList:

Average milliseconds per run for Collections::addAll: 3586.153
Average milliseconds per run for LinkedList::addAll: 6900.910

Here are the final outputs from the time testing code for ArrayList:

Average milliseconds per run for Collections::addAll: 354.337
Average milliseconds per run for ArrayList::addAll: 272.990

So, here's what I'd like to know:

  1. Controlling for List type, why is LinkedList::addAll so much slower than Collections::addAll?
  2. Controlling for List type, why is ArrayList::addAll faster than Collections::addAll?
Avi
  • 2,611
  • 1
  • 14
  • 26
  • 1
    Your measurement is flawed and can not be interpreted. You are not measuring your methods but primarily unrelated side-effects like JVM warmup, garbage collection (`System.gc()` does not run the collection) and most importantly JIT and cache effects. Do a correct benchmark and the results will be completely different. – Zabuzard Aug 29 '19 at 15:13
  • @Zabuza You can find the complete code [here](https://repl.it/repls/WearablePointedSorting). I do time garbage collection, but it is not tracked for the purposes of calculating average compute time. JVM warmup effects seem to disappear after the first iteration or so. Cache effects I'm not entirely sure how to deal with. – Avi Aug 29 '19 at 15:16
  • Just grab any of the benchmarking frameworks, jmh was even included in the JDK now. Then your results will be useable, and likely still be completely different. Warmup needs a couple of thousand iterations, garbage collection is really hard to trigger yourself. `System.gc()` can not really be used for this. There are sooo many things to consider (see the linked duplicate). Just take `jmh` and do it with that tool, its quite simple. – Zabuzard Aug 29 '19 at 15:19
  • Well, have a look at the code of `ArrayList::addAll` and `Collections::addAll` - you'll see that `ArrayList` directly operates on arrays while `Collections` needs to call `add()` for each new element. - That could be one explanation _if_ the times are still that different with a proper micro benchmark. – Thomas Aug 29 '19 at 15:22
  • Further note that your approach using `LinkedList::addAll` converts the new element array to a list and that will internally be converted back to an array - something that `Collections::addAll` won't do. – Thomas Aug 29 '19 at 15:28

0 Answers0