Sunday, October 6, 2013

A first look at Java 8's Lambdas

Cameron McKenzie has written an article on Java 8's new Lambdas so I thought I'd take a look at the new JDK. It's available here but note you may have IDE problems while the Java community decide on the exact syntax. I had to upgrade my IntelliJ to 12.1.5 to get the new syntax recognised.

McKenzie says that using Lambdas produces more efficient code but in my own tests, I haven't seen this. I took his code, only slightly modified it to remove System.out.println and put it into a Caliper test such that it looked like this:

    public int timeLambdas(int reps) {
        final Set   groups  = initGroups(reps);
        final List persons = initPersons(groups, reps);

        Stream sorted = persons.stream()
                .filter(p -> p.getAge() < reps)
                .map(p -> p.getGroup())
                .distinct()
                .sorted(comparing(g -> g.getSize()));

        int doNotJit = sorted.map(g -> g.getSize()).reduce(0, (i, j) -> i |= j);
        return doNotJit;
    }

(Note: I had to map/reduce the data otherwise all the filtering, mapping and dupe-removal does not get called. Lambdas are lazy and the code will not execute until asked to actually do something. I was very confused for an hour as I scratched my head wondering how on Earth they had made it so 'efficient'...).

Similarly, I had to make a slight modification to the traditional way of Java coding:

    public int timeTraditional(int reps) {
        final Set   myGroups = initGroups(reps);
        final List persons  = initPersons(myGroups, reps);

        Set groups = new HashSet<>();
        for (Person p : persons) {
            if (p.getAge() < reps)
                groups.add(p.getGroup());
        }
        List sorted = new ArrayList<>(groups);
        Collections.sort(sorted, new Comparator() {
            public int compare(Group a, Group b) {
                return Integer.compare(a.getSize(), b.getSize());
            }
        });
        int doNotJit = 1;
        for (Group g : sorted) {
            doNotJit |= g.getSize();
        }
        return doNotJit;
    }

So, running these in Caliper produced:

     0% Scenario{vm=/Users/phenry/Tools/JVMs/jdk1.8.0/bin/java, trial=0, benchmark=Lambdas} 753.72 ns; σ=98.39 ns @ 10 trials
     50% Scenario{vm=/Users/phenry/Tools/JVMs/jdk1.8.0/bin/java, trial=0, benchmark=Traditional} 530.16 ns; σ=60.72 ns @ 10 trials

     benchmark  ns linear runtime
     Lambdas 754 ==============================
     Traditional 530 ===================== 

Hmm, that's somewhat disappointing.

With a little bit of effort, I made the traditional approach even more efficient. I don't yet know enough about the new Lambdas to do the same for them but I suspect that there is more autoboxing going on as I mentioned before in other attempts to bring functional programming to the Java language. After all, the reduce method on the Stream interface (the gateway drug to Java 8's functional style) looks like this:

    T reduce(T identity, BinaryOperator accumulator);

(Note how that BinaryOperator is mapped to (i, j) -> ... )

That T represents a Class yet in my use of it in reduce(0, (i, j) -> i |= j), it's a a primitive int (0). Maybe, they'll optimise this in the future.

No comments:

Post a Comment