Optimizing Streams: Autoboxing

September 18, 2017

What is Autoboxing and why should I care?

Java 8 Streams are powerful tools, but as with any tools, you have to use them properly. To use any tool in a professional way, you have to understand how that tool works.
In Java variables can be grouped into two big categories: primitives and objects.For example: int i = 42; is a primitive and Integer i = 42; is an object. As you can see, the value of the variable in both cases 42, but for the Integer it is transformed into an object. This automatic conversion between the primitive types and their corresponding object wrapper classes done by the compiler is called Autoboxing.

Autoboxing and streams

Let’s take the following task as an example: you need to select the even numbers from a list of integers.
The following code using a stream is doing exactly that:

public static List<Integer> testParallelWithAutoboxing() {
  return input.parallelStream()
    .filter(n -> n % 2 == 0)
    .sorted()
    .collect(toList());
}

This looks good, but let’s consider this line: filter(n -> n % 2 == 0). Here the Integer will be unboxed into an int to perform the filtering logic via filter, then it will be converted back into an Integer.  Then the same thing happens with sorted(). All this unnecessary boxing and unboxing has a negative effect on performance. How negative? Keep reading and you will see.

Primitive streams

To avoid the unnecessary boxing / unboxing and improve performance, the stream of Integers needs to be converted into a stream of primitives.

public static List<Integer> testParallel() {
  return input.parallelStream()
    .mapToInt(n -> n)
    .filter(n -> n % 2 == 0)
    .sorted()
    .boxed()
    .collect(toList());
}

With the help of mapToInt it is possible to create a primitive stream, an IntStream. After this, the filtering and sorting is done. Notice, that now we call the IntSteam.filter and IntStream.sorted, because of the conversion from Stream<Integer> to IntStream. To return a list with the help of collect, we need to transform the IntStream back into a Stream<Integer>by calling boxed().

Benchmarking

Time to see how much does it improve the speed if the unnecessary autoboxing is taken care of.
After generating 600,000 random Integers, I compared the performance of 5 measurements after 5 warmup iterations with JMH of:

  • Serial stream with unnecessary autoboxing (testSerialWithAutoboxing)
  • Serial stream without unnecessary autoboxing (testSerial)
  • Parallel stream with unnecessary autoboxing (testParallelWithAutoboxing)
  • Parallel stream without unnecessary autoboxing (testParallel)
import org.openjdk.jmh.annotations.*;

import java.util.List;
import java.util.Random;

import static java.util.stream.Collectors.toList;

public class ParallelStreamBenchmark {
  private static final int NUMBER_OF_RANDOM_NUMBERS = 600_000;
  private static List<Integer> input = generateRandomNumbers(0, Integer.MAX_VALUE - 1, NUMBER_OF_RANDOM_NUMBERS);

  @Benchmark
  @BenchmarkMode(value = Mode.AverageTime)
  @Warmup(iterations = 5)
  @Measurement(iterations = 5)
  @Fork(value = 1)
  public static List<Integer> testSerialWithAutoboxing() {
    return input.stream()
      .filter(n -> n % 2 == 0)
      .sorted()
      .collect(toList());
  }

  @Benchmark
  @BenchmarkMode(value = Mode.AverageTime)
  @Warmup(iterations = 5)
  @Measurement(iterations = 5)
  @Fork(value = 1)
  public static List<Integer> testSerial() {
    return input.stream()
      .mapToInt(n -> n)
      .filter(n -> n % 2 == 0)
      .sorted()
      .boxed()
      .collect(toList());
  }

  @Benchmark
  @BenchmarkMode(value = Mode.AverageTime)
  @Warmup(iterations = 5)
  @Measurement(iterations = 5)
  @Fork(value = 1)
  public static List<Integer> testParallelWithAutoboxing() {
    return input.parallelStream()
      .filter(n -> n % 2 == 0)
      .sorted()
      .collect(toList());
  }

  @Benchmark
  @BenchmarkMode(value = Mode.AverageTime)
  @Warmup(iterations = 5)
  @Measurement(iterations = 5)
  @Fork(value = 1)
  public static List<Integer> testParallel() {
    return input.parallelStream()
      .mapToInt(n -> n)
      .filter(n -> n % 2 == 0)
      .sorted()
      .boxed()
      .collect(toList());
  }

  public static List<Integer> generateRandomNumbers(int min, int max, int limit) {
    Random random = new Random();
    return random.ints(min, max + 1).limit(limit).boxed().collect(toList());
  }
}

The results are:

Benchmark                                           Mode  Cnt  Score    Error  Units
ParallelStreamBenchmark.testParallel                avgt    5  0.009 ±  0.001   s/op
ParallelStreamBenchmark.testParallelWithAutoboxing  avgt    5  0.018 ±  0.002   s/op
ParallelStreamBenchmark.testSerial                  avgt    5  0.033 ±  0.003   s/op
ParallelStreamBenchmark.testSerialWithAutoboxing    avgt    5  0.081 ±  0.006   s/op

Conclusion

Because this is a task which could be solved efficiently in a parallel way by the fork/join threadpool of the parallel streams, it is about 3,5x faster than a serial stream.
What is worth noting is that by avoiding extensive autoboxing, the performance of both the serial and parallel solution was vastly increased. Of course the actual increase depends on many things, such as the number of elements in the stream which will get autoboxed, but you should remember the cost of autoboxing, because if you can avoid it, you can improve the performance of your code rather easily.

András Döbröntey

About the Author

András Döbröntey

Leave a Comment:

Bitnami