Friday-Benchmarking Functional Java
Lets image our product owner goes crazy one day and ask to you to do the following :
From a set of Strings as follows :
"marco_8", "john_33", "marco_1", "john_33", "thomas_5", "john_33", "marco_4", ....
give me a comma separated String with only the marco's numbers and numbers need to be in order.
Example of expected result : "1,4,8"
I will implement this logic in 4 distinct ways and I will micro benchmark each one of them. The ways I’m going to implement the logic are :
- Traditional java with loops and all.
- Functional with Guava
- Functional with java 8 stream
- Functional with java 8 parallelStream
Code is below or in gist
package com.marco.brownbag.functional; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicates; import com.google.common.collect.Collections2; import com.google.common.collect.Ordering; public class MicroBenchMarkFunctional { private static final int totStrings = 2; public static void main(String[] args) { Set<String> someNames = new HashSet<String>(); init(someNames); for (int i = 1; i < totStrings; i++) { someNames.add("marco_" + i); someNames.add("someone_else_" + i); } System.out.println("start"); run(someNames); } private static void run(Set<String> someNames) { System.out.println("========================"); long start = System.nanoTime(); int totalLoops = 20; for (int i = 1; i < totalLoops; i++) { classic(someNames); } System.out.println("Classic : " + ((System.nanoTime() - start)) / totalLoops); start = System.nanoTime(); for (int i = 1; i < totalLoops; i++) { guava(someNames); } System.out.println("Guava : " + ((System.nanoTime() - start)) / totalLoops); start = System.nanoTime(); for (int i = 1; i < totalLoops; i++) { stream(someNames); } System.out.println("Stream : " + ((System.nanoTime() - start)) / totalLoops); start = System.nanoTime(); for (int i = 1; i < totalLoops; i++) { parallelStream(someNames); } System.out.println("Parallel Stream : " + ((System.nanoTime() - start)) / totalLoops); System.out.println("========================"); } private static void init(Set<String> someNames) { someNames.add("marco_1"); classic(someNames); guava(someNames); stream(someNames); parallelStream(someNames); someNames.clear(); } private static String stream(Set<String> someNames) { return someNames.stream().filter(element -> element.startsWith("m")).map(element -> element.replaceAll("marco_", "")).sorted() .collect(Collectors.joining(",")); } private static String parallelStream(Set<String> someNames) { return someNames.parallelStream().filter(element -> element.startsWith("m")).map(element -> element.replaceAll("marco_", "")).sorted() .collect(Collectors.joining(",")); } private static String guava(Set<String> someNames) { return Joiner.on(',').join( Ordering.from(String.CASE_INSENSITIVE_ORDER).immutableSortedCopy( Collections2.transform(Collections2.filter(someNames, Predicates.containsPattern("marco")), REPLACE_MARCO))); } private static Function<String, String> REPLACE_MARCO = new Function<String, String>() { @Override public String apply(final String element) { return element.replaceAll("marco_", ""); } }; private static String classic(Set<String> someNames) { List<String> namesWithM = new ArrayList<String>(); for (String element : someNames) { if (element.startsWith("m")) { namesWithM.add(element.replaceAll("marco_", "")); } } Collections.sort(namesWithM); StringBuilder commaSeparetedString = new StringBuilder(); Iterator<String> namesWithMIterator = namesWithM.iterator(); while (namesWithMIterator.hasNext()) { commaSeparetedString.append(namesWithMIterator.next()); if (namesWithMIterator.hasNext()) { commaSeparetedString.append(","); } } return commaSeparetedString.toString(); } }
Two points before we dig into performance :
- Forget about the init() method, that one is just to initialize objects in the jvm otherwise numbers are just crazy.
- The java 8 functional style looks nicer and cleaner than guava and than developing in a traditional way!
Performance:
Running that program on my mac with 4 cores, the result is the following :
======================== Classic : 151941400 Guava : 238798150 Stream : 151853850 Parallel Stream : 55724700 ========================
Parallel Stream is 3 times faster. This is because java will split the job in multiple tasks (total of tasks depends on your machine, cores, etc) and will run them in parallel, aggregating the result at the end.
Classic Java and java 8 stream have more or less the same performance.
Guava is the looser.
That is amazing, so someone could think: “cool, I can just always use parallelStream and I will have big bonus at the end of the year”.
But life is never easy. Here is what happens when you reduce that Set of strings from 200.000 to 20:
======================== Classic : 36950 Guava : 69650 Stream : 29850 Parallel Stream : 143350 ========================
Parallel Stream became damn slow. This because parallelStream has a big overhead in terms of initializing and managing multitasking and assembling back the results.
Java 8 stream looks now the winner compare to the other 2.
Ok, at this point, someone could say something like : “for collections with lots of elements I use parallelStream, otherwise I use stream.”
That would be nice and simple to get, but what happens when I reduce that Set again from 20 to 2?
This :
======================== Classic : 8500 Guava : 20050 Stream : 24700 Parallel Stream : 67850 ========================
Classic java loops are faster with very few elements.
So at this point I can go back to my crazy product owner and ask how many Strings he thinks to have in that input collection. 20? less? more? much more?
Like the Carpenter says : Measure Twice, Cut Once!!
Reference: | Friday-Benchmarking Functional Java from our JCG partner Marco Castigliego at the Remove duplication and fix bad names blog. |