Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The costs of streams and closures in Java 8

I'm rewriting an application that involves dealing with objects in order of 10 millions using Java 8 and I noticed that streams can slow down the application up to 25%. Interestingly, this happens when my collections are empty as well, so it's the constant initialization time of stream. To reproduce the problem, consider the following code:

    long start = System.nanoTime();
    for (int i = 0; i < 10_000_000; i++) {
        Set<String> set = Collections.emptySet();
        set.stream().forEach(s -> System.out.println(s));
    }
    long end = System.nanoTime();
    System.out.println((end - start)/1000_000);

    start = System.nanoTime();
    for (int i = 0; i < 10_000_000; i++) {
        Set<String> set = Collections.emptySet();
        for (String s : set) {
            System.out.println(s);
        }
    }
    end = System.nanoTime();
    System.out.println((end - start)/1000_000);

The result is as follows: 224 vs. 5 ms.

If I use forEach on set directly, i.e., set.forEach(), the result will be: 12 vs 5ms.

Finally, if I create the closure outside once as

Consumer<? super String> consumer = s -> System.out.println(s);

and use set.forEach(c) the result will be 7 vs 5 ms.

Of course, the nubmers are small and my benchmarking is very primitive, but does this example shows that there is an overhead in initializing streams and closures?

(Actually, since set is empty, the initialization cost of closures should not be important in this case, but nevertheless, should I consider creating closures before hand instead of on-the-fly)

like image 1000
Wickoo Avatar asked Dec 31 '14 02:12

Wickoo


1 Answers

The cost you see here is not associated with the "closures" at all but with the cost of Stream initialization.

Let's take your three sample codes:

for (int i = 0; i < 10_000_000; i++) {
    Set<String> set = Collections.emptySet();
    set.stream().forEach(s -> System.out.println(s));
}

This one creates a new Stream instance at each loop; at least for the first 10k iterations, see below. After those 10k iterations, well, the JIT is probably smart enough to see that it's a no-op anyway.

for (int i = 0; i < 10_000_000; i++) {
    Set<String> set = Collections.emptySet();
    for (String s : set) {
        System.out.println(s);
    }
}

Here the JIT kicks in again: empty set? Well, that's a no-op, end of story.

set.forEach(System.out::println);

An Iterator is created for the set, which is always empty? Same story, the JIT kicks in.

The problem with your code to start with is that you fail to account for the JIT; for realistic measurements, run at least 10k loops before measuring, since 10k executions is what the JIT requires to kick in (at least, HotSpot acts this way).


Now, lambdas: they are call sites, and they are linked only once; but the cost of the initial linkage is still there, of course, and in your loops, you include this cost. Try and run only one loop before doing your measurements so that this cost is out of the way.

All in all, this is not a valid microbenchmark. Use caliper, or jmh, to really measure the performance.

An excellent video to see how lambdas work here. It is a little old now, and the JVM is much better than it was at this time with lambdas.

If you want to know more, look for literature about invokedynamic.

like image 148
fge Avatar answered Sep 22 '22 11:09

fge