Costs of threads and closures in Java 8

I am rewriting an application that includes working with objects of the order of 10 million using Java 8, and I noticed that threads can slow the application down to 25%. Interestingly, this happens when my collections are also empty, so this is a constant thread initialization time. 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 directly, i.e. set.forEach() , the result will be: 12 vs 5 ms.

Finally, if I create a 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, nubmers are small, and my benchmarking is very primitive, but does this example show that there are overheads in the initialization of threads and closures?

(Actually, since set empty, the cost of initializing closures should not be important in this case, but, nevertheless, I should consider the possibility of creating closures before the distribution, and not on the fly)

+6
source share
1 answer

The cost that you see here is not related to the "closure" in general, but to the cost of Stream initialization.

Let's take three code samples:

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

This creates a new Stream instance in each loop; at least for the first 10k iterations, see below. After these 10k iterations, well, the JIT is probably smart enough to see that it is 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 starts up again: empty set? Well, this is no-op, the end of the story.

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

Is an Iterator for a set that is always empty? Same story, JIT kicks.

The problem with your code, for starters, is that you do not take JIT into account; for realistic measurements, run at least 10k loops before the measurement, since 10k execution is what the JIT requires (at least HotSpot acts this way).


Now, lambdas: these are call sites, and they are connected only once; but the cost of the initial connection still exists, of course, and in your cycles you include this cost. Try to run only one cycle before taking measurements so that this cost is aloof.

In general, this is not a valid micro start. Use a caliper or jmh to really measure performance.

Great video to see how lambdas work here . Now this is a bit old-fashioned, and the JVM is much better than lambdas at the time.

If you want to know more, check out the invokedynamic .

+7
source

Source: https://habr.com/ru/post/980340/


All Articles