Live data from Hacker News

State of the Lambda

cr.openjdk.java.net

31–40 of 44 posts

Re: State of the Lambda

#31
post #27

Earlier quoted context omitted.

520ms was for the full dynamic version. But we did have a snafu. The 'groovy' program seems to choose its JVM in an odd way, it was using Apple's 1.6 JVM despite 'java -version' returning the 1.7 JVM. I fixed this by setting JAVA_HOME and now get ~300ms for the full dynamic version, 22ms for explicit types and 586ms for the @CompileStatic version. I'm guessing my method of running this is not compatible with @Compile…

I actually ran through the intellij version 12 EAP "LEDA" which has Groovy 2 in it for most of the tests. The @compilestatic stuff should work regardless, but you do need to import the compilestatic transform. import groovy.transform.CompileStatic

Well I ran it in IntelliJ and saw the same results. Then I found the problem...

I used compile_static3.groovy from your repo for the test which contains the following:

  bench(d, "fib_dynamic", 30, "Full dynamic ")
  bench(d, "fib_integer", 30, "Groovy w/ explicit types")
  bench(d, "fib", 40, "Explicit type w/@CompileStatic")
Notice the '40' on the last line :p

Corrected results:

  Full dynamic  took 305 ms
  Groovy w/ explicit types took 22 ms
  Explicit type w/@CompileStatic took 7 ms
Which is much more like what I expected compared to Ruby at 475ms.

Re: State of the Lambda

#32
post #31

Earlier quoted context omitted.

I actually ran through the intellij version 12 EAP "LEDA" which has Groovy 2 in it for most of the tests. The @compilestatic stuff should work regardless, but you do need to import the compilestatic transform. import groovy.transform.CompileStatic

Well I ran it in IntelliJ and saw the same results. Then I found the problem... I used compile_static3.groovy from your repo for the test which contains the following: bench(d, "fib_dynamic", 30, "Full dynamic ") bench(d, "fib_integer", 30, "Groovy w/ explicit types") bench(d, "fib", 40, "Explicit type w/@CompileStatic") Notice the '40' on the last line :p Corrected results: Full dynamic took 305 ms Groovy w/ explici…

But.... this was in my verbal presentation - move that @compilestatic benchmark to the first test - it'll be different. There's something about running it after the other two which is adding to the benefit - caching maybe? Moving it to the first one gives me 18-20ms generally. Try it :)

Re: State of the Lambda

#33
post #31

Earlier quoted context omitted.

Well I ran it in IntelliJ and saw the same results. Then I found the problem... I used compile_static3.groovy from your repo for the test which contains the following: bench(d, "fib_dynamic", 30, "Full dynamic ") bench(d, "fib_integer", 30, "Groovy w/ explicit types") bench(d, "fib", 40, "Explicit type w/@CompileStatic") Notice the '40' on the last line :p Corrected results: Full dynamic took 305 ms Groovy w/ explici…

But.... this was in my verbal presentation - move that @compilestatic benchmark to the first test - it'll be different. There's something about running it after the other two which is adding to the benefit - caching maybe? Moving it to the first one gives me 18-20ms generally. Try it :)

Just a guess but the JIT may be detecting the recursion and replacing it with a loop. The recursion signature is the same for each method so the JIT may be able to catch that.

Letting the JIT warm up is enlightening though, I looped the 3 tests 1000 times and also looped my ruby test 1000 times and I get this:

  Explicit type w/@CompileStatic took 5 ms
  Groovy w/ explicit types took 11 ms
  Full dynamic  took 75 ms

  Ruby took 464ms

Re: State of the Lambda

#34

> It is our intent to prohibit capture of mutable local variables. The reason is that idioms like this: int sum = 0; list.forEach(e -> { sum += e.size(); }); > are fundamentally serial An unfortunate decision. Yes, updating such a variable from multiple threads can cause race conditions. But there are many ways to run into race conditions when writing multithreaded code, and this doesn't really add a new one. Further…

Ironically, "sum += e.size()" is conceptually not serial at all; the order that each iteration runs is not relevant while you could restructure the computation into a tree of partial aggregations.

The work around in Java, using a Cell, is a hack that gives you the updatable mutable variable you need without too much ugly overhead. so something like...

  cell = new Cell(); ... { ... cell.Value = ... } })

Re: State of the Lambda

#35

> It is our intent to prohibit capture of mutable local variables. The reason is that idioms like this: int sum = 0; list.forEach(e -> { sum += e.size(); }); > are fundamentally serial An unfortunate decision. Yes, updating such a variable from multiple threads can cause race conditions. But there are many ways to run into race conditions when writing multithreaded code, and this doesn't really add a new one. Further…

The motivation is that the question of whether list.forEach (or maybe better, iterable.forEach) is serial or parallel is to be left up to the implementation of the collection. Everything related to multi-threaded code since about Java 5 has been about hiding the details of concurrency and allowing programmers to think about code in a serial fashion, but still get the gains from multi threaded code.

It's not necessarily about parallelism either -- you could just as easily pass in a function as some sort of callback that gets called far in the future, for example,

String error = null; return createClientWithErrorHandler(e -> { error = e; });

What does that even mean? Does the execution context need to be kept hanging around?

Java already provides good atomic references if you need to have a sum variable or anything similar.

Re: State of the Lambda

#37
It actually looks pretty good. I can see they spend a lot of effort to make sure it fits into the existing system, keeping new syntax or new form to the minimum.

Re: State of the Lambda

#38

> It is our intent to prohibit capture of mutable local variables. The reason is that idioms like this: int sum = 0; list.forEach(e -> { sum += e.size(); }); > are fundamentally serial An unfortunate decision. Yes, updating such a variable from multiple threads can cause race conditions. But there are many ways to run into race conditions when writing multithreaded code, and this doesn't really add a new one. Further…

The motivation is that the question of whether list.forEach (or maybe better, iterable.forEach) is serial or parallel is to be left up to the implementation of the collection. Everything related to multi-threaded code since about Java 5 has been about hiding the details of concurrency and allowing programmers to think about code in a serial fashion, but still get the gains from multi threaded code. It's not necessari…

> What does that even mean? Does the execution context need to be kept hanging around?

Once the block containing the declaration of 'error' has been exited, the only way anything could refer to 'error' would be if there were at least one additional lambda expression in the same block that was closed over the same binding of 'error'. Two such lambda expressions could thus communicate with each other through the shared binding.

Re: State of the Lambda

#39
post #33

Earlier quoted context omitted.

But.... this was in my verbal presentation - move that @compilestatic benchmark to the first test - it'll be different. There's something about running it after the other two which is adding to the benefit - caching maybe? Moving it to the first one gives me 18-20ms generally. Try it :)

Just a guess but the JIT may be detecting the recursion and replacing it with a loop. The recursion signature is the same for each method so the JIT may be able to catch that. Letting the JIT warm up is enlightening though, I looped the 3 tests 1000 times and also looped my ruby test 1000 times and I get this: Explicit type w/@CompileStatic took 5 ms Groovy w/ explicit types took 11 ms Full dynamic took 75 ms Ruby to…

Very interesting - haven't compared it to Ruby, but the 75ms was interesting on its own. I knew dynamically Groovy would generally be faster than Ruby, but not by that much, given the runtime dynamism.

Thanks! (and thanks for the pull request fix)

Re: State of the Lambda

#40
post #15

I'm surprised more people aren't just using Groovy. The performance of Groovy 2.0 on JVM 7 is greatly improved to the point where its a non-issue in 95% of situations (and you can just drop to java for those portions with no penalties).

I tend to agree with you, but Groovy 2 is still pretty new. In my experience, most of the hardcode Java shops and devs that rejected Groovy (primarily?) because of speed are generally slow-adopters, and wouldn't be using Groovy 2 just because it was released a few months ago. As to speed, I've not benchmarked it on JDK7, so I'm not sure what effect the InvokeDynamic stuff has yet, but @CompileStatic annotation helps…

Groovy 2.0 was only released a few months ago, and isn't production ready. Groovy 2.0 has two jars in it, one with static-compilation and static-inference grafted on, and the other with optimizations for JDK 7's invoke-dynamic bytecode. Programmers can't use both static-compilation and invoke-dynamic in the same source file.

Also, both jars seem to have problems. Groovy 2.0.4 was released yesterday only 2 weeks after version 2.0.2 was released "to fix static compilation and type inference problems". Grails 2.2-rc-1 (actually the first beta and not a serious "release candidate" for production use, because the releases called "beta" are really alphas) has only just been released, the first with Groovy 2.x, with only the static-compilation jar from Groovy 2.0.2 bundled, not the invoke-dynamic jar.

The facilities of the invoke-dynamic jar might never be merged into the static-compilation jar. There seems to be some infighting at SpringSource over who's in control of Groovy. Version 2.0.3 was skipped because the P.M. "compiled it on the JDK 6 instead of JDK 7 by mistake", which looks like a political play by Guillaume Laforge, SpringSource's project manager for Groovy, against Jochen Theodorou, their technical lead who's done most of the programming work on Groovy since 2004.

A year ago, Laforge hired another developer, Cedric Champeau, to put static type inference and compilation into Groovy, in order to mitigate a SpringSource business threat by Alex Tkachman's Groovy++, a static-compilation addon to Groovy which has since been abandoned. Groovy++ had to switch from supporting Groovy 1.8 to 1.7 for a while during its development because of compatibility problems in subsequent beta releases of Groovy, which may have been deliberate, and at the very least, Groovy++ served as an "inspiration" for Groovy 2.0's static-compilation jar. It's looking like Theodorou's invoke-dynamic bytecode optimizations aren't being merged into the static-compilation jar because of a power play by Laforge and Champeau who control the static-compilation jar. The project manager is diluting Theodorou's technical importance to Groovy and thus increasing his own influence.

Concerning your benchmarks of Groovy with Java, you left out Scala and Clojure, also popular JVM languages, and you chose an algorithm not used much in business. Both Scala and Clojure have lazy-evaluation in their library lambdas/functions, as will Java 8. Groovy only enables strict evaluation, which slows down many computations used in real business programming scenarios, of which fibonacci isn't one. Lazy-evaluation isn't in the pipeline for Groovy anytime soon. Its "GDK" utility methods were mostly written early on by a programmer without the aptitude for developing anything too complex, such as lambdas with lazy-evaluation. Seems like JDK 8's lambdas, due to ship in about a year, will be the first time Groovy will enable lazily-evaluated computations, long after Scala and Clojure. As for the invoke-dynamic bytecode optimizations being merged into the main jar used by Groovy 2.0 and Grails 2.2, JRuby's long stolen that show. It might never happen with Groovy.

And of course there's no spec for Groovy, unlike Scala's detailed spec or Clojure's function doc-strings. Without a spec, Groovy is still single-platform, unlike Clojure and Scala, and many other JVM languages that began their life somewhere else first.

So I don't think slow adoption by Java shops that were concerned about Groovy's performance is the reason more businesses don't adopt Groovy. It's more likely that the static-compilation code isn't production ready, just laden with problems, political infighting at SpringSource, trailing behind other languages in lazy-evaluation of lambdas (Clojure, Scala) and invoke-dynamic optimizations (JRuby), an informal-only spec that changes between point releases, the names "Groovy" and "G-Strings" evoking drug use and discrimination against female employees, and a general sense by technical managers that there's a price to be paid later down the line if they adopt Groovy.

Post reply on HN