Friday 12 September 2014

Friday-Benchmarking Functional Java

Lets image your product owner goes crazy one day and he asks 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 back 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 = 200000;

        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 :

  1. Forget about the init() method, that one is just to initialize objects in the jvm otherwise numbers are just crazy.
  2. 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!!

 

 







No comments:

Post a Comment