Thoughts, Rant and Code

Curious Case of Streams in Java 8

I have been experimenting with Java 8 steams API and thinking about using it in projects I work on. Found the API to create a parallel stream from a list attractive utility for some computations.

I have been also thinking of the overhead of a parallel stream for not so large datasets.

One thing that intrigued me was the ability to get a parallel stream from a spliterator, which is available on available on all Iterable.

Did some experiments with parallel streams and noticed some real performance loss for some Iterable. The reason is, spliterator decides how fast the parallel stream can be. One of the job spliterator has to do is to decompose data into two parts, which can be processed parallel.

For example, in case of a array backed list, spliterator can determine how data can be decomposed in to two parts finely before hand. However, in case of linked list, spliterator can never be precise about the length of the data set. So it splits the data into first and rest, which is as bad as it can get.

Below is the code used for benchmarking using JMH.

The test class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ParallelStreamSupportTest {

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void intStream(IntStreamProvider provider) {
        provider.getStream().mapToInt(i -> i + 2).sum();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void streamFromContiguousSet(ContiguousStreamProvider provider) {
        provider.getStream().mapToInt(i -> i + 2).sum();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void listIterator(StreamOverListProvider provider) {
        provider.getStream().mapToInt(i -> i + 2).sum();
    }

}

The provider interface.

1
2
3
4
5
6
7
public interface StreamProvider {

    final static int SIZE = 10000000;

    public Stream<Integer> getStream();

}

Provider for IntStream

1
2
3
4
5
6
7
@State(Scope.Thread)
public class IntStreamProvider implements StreamProvider {

    public Stream<Integer> getStream() {
        return IntStream.rangeClosed(1, SIZE).boxed().parallel();
    }
}

Provider for a stream from ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@State(Scope.Thread)
public class StreamOverListProvider implements StreamProvider {

    private List<Integer> integers;

    public StreamOverListProvider() {
        Set<Integer> integer = ContiguousSet.create(Range.closed(1, SIZE), DiscreteDomain.integers());
        integers = new ArrayList<>();
        integers.addAll(integer);
    }

    public Stream<Integer> getStream() {
        return integers.parallelStream();
    }
}

Provider for a stream from a Set

1
2
3
4
5
6
7
8
9
10
11
12
13
@State(Scope.Thread)
public class ContiguousStreamProvider implements StreamProvider {

    private Set<Integer> integers;

    public ContiguousStreamProvider() {
        integers = ContiguousSet.create(Range.closed(1, SIZE), DiscreteDomain.integers());
    }

    public Stream<Integer> getStream() {
        return StreamSupport.stream(integers.spliterator(), true);
    }
}

Below is the benchmark result.

1
2
3
4
Benchmark                                                Mode  Samples   Score   Error  Units
o.s.ParallelStreamSupportTest.intStream                  avgt      200  26.438 ± 0.404  ms/op
o.s.ParallelStreamSupportTest.listIterator               avgt      200  15.549 ± 0.355  ms/op
o.s.ParallelStreamSupportTest.streamFromContiguousSet    avgt      200  56.330 ± 0.309  ms/op

From the result it quite visible that a ParallelStream over an ArrayList is the fastest one, with 15.549 ms/op. However, the benchmarking code has a flaw, where the collection behind IntStream is constructed for each iteration, where as the actual collection for other streams are pre-constructed and only stream is constructed for each iteration. But since that is not the purpose of benchmarking, I decided to ignore that, as only relative performance need to be known.

So one could consider IntStream and Stream over an ArrayList being equally fast. But stream over a Set is much slower compared to others.