Re:Clojure, Faster
by dwohnitmok on Friday January 08, 2021 @03:44PM
(#25690205)
These are good strategies for speeding up Clojure code, and are great if you have a small handful of hotspots where performance really matter. However, if you constantly find yourself resorting to these strategies and you're in a team environment, I would recommend just writing everything in Java and slapping a Clojure wrapper on top.
The main problem is that some of these optimizations are very brittle in combination with one another and require a good deal of care to maintain their effectiveness. A person unfamiliar with your codebase who just marches in and starts writing idiomatic Clojure can inadvertently deoptimize large swaths of your codebase.
This is a problem that afflicts a lot of languages that have a wide variance in performance between idiomatic-but-slow and idiomatic-but-fast (or close to idiomatic) variants of the same code. Haskell is another good example of this, which is perhaps even more extreme in that very fast Haskell code can look perfectly idiomatic, but an otherwise innocent-looking change can crash everything to the ground.
That variance is always going to exist to some degree in every language, but if you find that you need to always or almost always be on the latter side of that divide to meet your performance targets, maintenance and knowledge transfer is going to be a huge pain. You might want to consider rewriting it in a different language that shifts the variance further down field so that you're back on the easy side of that divide for most of your codebase.
Re:Clojure, Faster
by fulafel on Friday January 08, 2021 @04:37PM
(#25690831)
This is an argument worth considering, but on the other hand keeping track of performance regressions using CI is not hard and requires the team to master a new language that isn't the first choice for other code in the project.
Re:Clojure, Faster
by dwohnitmok on Friday January 08, 2021 @05:25PM
(#25691328)
I disagree in the particular case of Clojure and Java (there are other places where I would agree, famously Jane Street gets by with staying in OCaml as much as possible and writing copious amounts of C-ish OCaml where necessary).
> keeping track of performance regressions using CI is not hard
It's actually kind of hard on the JVM. Again, if you have only a few hotspots then it's easy enough to set up some tests, but if you really need coverage over most of your codebase, it can get tricky, especially because these optimizations can combine in nonlocal ways so just exercising one function in one context doesn't guarantee the same performance in another. When taking into account warming up JIT compilation, benchmarks can become a real time suck. Criterium runs take a long time. You can't pop them like candy in the same way you can with unit tests, otherwise you can easily face hour+ CI runs.
The usual way I've seen this work successfully is if you get lucky and your project has very few overall code paths that you can test with e.g. integration tests and you can catch essentially all perf regressions there.
> requires the team to master a new language that isn't the first choice for other code in the project
I think you really need to have a good grasp of Java to write Clojure well (or you'll develop it as you write more Clojure). Both because the ecosystem makes liberal use of Java libraries and because otherwise none of the stack traces or even the optimization decisions listed in this article will really make sense. Clojure doesn't do much to hide Java from you (which has its own set of pros and cons).
Re:Clojure, Faster
by fulafel on Saturday January 09, 2021 @03:58AM
(#25698761)
> I think you really need to have a good grasp of Java to write Clojure well (or you'll develop it as you write more Clojure).
My experience is pretty much the opposite. Java programming experience is not needed. Of course you'll develop your understanding and experience of the JVM platform and some Java APIs as a user over time but that's still far from being comfortable writing Java code. I'll concede that optimizing with annotations is helped by Java experience, especially is it includes Java perf work.
Re:Clojure, Faster
by patrec on Friday January 08, 2021 @04:58PM
(#25691064)
This reads a bit like "why you shouldn't use clojure" to me. I'd expect to leave some performance on the table when writing idiomatic code in a dynamic language with immutable datatypes, but a function that's already specialized to arrays, as the name indicates, taking 3 orders of magnitude too much time? 1-10 microseconds to say "get me the length of this java array, and BTW this is a java array"? What on earth is going on there?
Edit: So I just ran the author's (alength (long-array 5)) (with the indirection to foil type inference) and got 10µs mean execution time in criterium. By contrast here's some roughly equivalent python:
In [5]: a = array.array('l', [5])
In [6]: timeit len(a)
32.9 ns ± 0.105 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
More or less exactly the same with a plain python list. So clojure's type specialized function manages to be 300 times slower than the generic version in freaking *python*. In fairness "count" is better if still terrible (4x slower than python).
Re:Clojure, Faster
by aphyr on Friday January 08, 2021 @05:27PM
(#25691353)
In short: it's slow because there isn't just one kind of Java array, and if you don't say which one you mean, Clojure has to do reflection to figure it out.
Inside the JVM, there are several different types of arrays: bytes, floats, objects, etc. Clojure.lang.RT includes methods for getting the length of all of these types--for instance, here's the method for the length of arrays of longs:
The clojure.core/alength function calls clojure.lang.RT/alength, but that callsite is polymorphic: the Clojure compiler doesn't know which of the RT functions to emit a call for, because it needs the type signature. If the type is unavailable at compile time, the Clojure compiler emits reflective code which inspects the type of the reference, determines which specific clojure.lang.RT/alength implementation to dispatch to, then executes that call dynamically. That's the slow part!
Just like the article says, if you include a type hint, the compiler can emit an invokestatic call directly to, say, clojure.lang.RT.alength(long[]), and skip all the reflection.
Re:Clojure, Faster
by patrec on Friday January 08, 2021 @06:35PM
(#25692228)
> In short: it's slow because there isn't just one kind of Java array, and if you don't say which one you mean, Clojure has to do reflection to figure it out.
But it can't need any less reflection than "count" needs, and "count" is 100x faster. Surely whatever alength is doing in the non-type hinted case can't be... ideal?
> If the type is unavailable at compile time, the Clojure compiler emits reflective code which inspects the type of the reference, determines which specific clojure.lang.RT/alength implementation to dispatch to, then executes that call dynamically. That's the slow part!
But where do all those clock cycles go? Doing runtime type inspection and then dispatching to one of 9 static functions based on that doesn't feel like it ought to be anywhere near that terrible (especially in a JIT'ed language).
Re:Clojure, Faster
by patrec on Friday January 08, 2021 @06:49PM
(#25692600)
Just to illustrate, some pretend-analogous run time type-inspect based dispatch code in python (a language not known for its blazing speed) is still much, much faster than this:
In [3]: timeit a = [1]; a.__len__() if type(a) is list else len(a)
126 ns ± 0.615 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Re:Clojure, Faster
by aphyr on Friday January 08, 2021 @05:40PM
(#25691466)
So clojure's type specialized function manages to be 300 times slower than the generic version in freaking python. In fairness "count" is better if still terrible (4x slower than python).
Er, to be clear, Clojure's `alength` is a generic function--you're not measuring type-specialized behavior. The type-specialized `alength` call clocks in at around ~4 ns/call, as the article notes.
Re:Clojure, Faster
by patrec on Friday January 08, 2021 @06:07PM
(#25691693)
Well, it's generic in the type of the array element, but it's not generic in that it only works with Java arrays not arbitrary sequences. In particular it's clearly less generic than count (in clojure) or len (in python).
And it is not at all obvious to someone not intimately familiar with the JVM/Clojure that the element type would in any way matter for working out the length of an array. Indeed as far as I'm aware the OpenJVM memory representation for arrays has a length field at the same offset that's independent of the array element type, correct?
So that alength (in the typical case) is in fact 300x slower than len in python and 100x than count in clojure does not seem something that would be intuitively obvious to most people (including clojure programers).
Re:Clojure, Faster
by gameswithgo on Friday January 08, 2021 @05:44PM
(#25691502)
It is really nice when the easy way is also the fast way. I have a pet peeve about this for languages that have “iterators”. they should be fast! like rust and c++ and java streams manage to make then. then you don’t have to worry about this stuff.
Re:Clojure, Faster
by fulafel on Monday January 11, 2021 @02:25AM
(#25725788)
The Clojure functional equivalent, approximately, of iterators is the ISeq interface, and lazy sequences etc that work with it. It's fast. You just don't have to deal with iterators manually.
Re:Clojure, Faster
by brundolf on Friday January 08, 2021 @06:04PM
(#25691668)
You wouldn't want to use iterators to compute the length of a collection, even in a language where they're fast. That turns a constant-time operation into a linear-time operation
Re:Clojure, Faster
by patrec on Saturday January 09, 2021 @07:12AM
(#25699715)
Not true, with two random access iterators in C++ for example you can compute the distance between them, and thus the the length of an array (e.g. a.end() - a.begin()) and it will be constant time.
Re:Clojure, Faster
by brundolf on Sunday January 10, 2021 @12:09PM
(#25716757)
I guess I'm not familiar enough with C++'s iterators. In most languages I'm familiar with, an iterator is an abstract concept that only knows how to get the next item and (sometimes) tell whether or not it's finished.
Re:Clojure, Faster
by eigenhombre on Friday January 08, 2021 @07:22PM
(#25693197)
A nice post full of helpful insights and techniques (no.disassemble is particularly fun, I think).
To be honest, I find (JVM) Clojure is pretty fast, as mentioned elsewhere in the comments. Once my program has gotten off the ground -- most of the time my biggest wish is for faster startup time, and lately Babashka (an interpreted Clojure implemented with GraalVM, which starts more or less instantly) is my go-to for small, short programs I run often ... "scripts", I guess, though Clojure is expressive to do stuff in Babashka that I wouldn't dream of doing in Bash.
Re:Clojure, Faster
by rcgorton on Friday January 08, 2021 @05:10PM
(#25691180)
I'm concerned about these 'benchmarks'. They are using nanosecond timing intervals. By design, CPU clocks have jitter to resist timing attacks. Secondly, a single clock tick is much more than a nanosecond.
I would feel much more comfortable with performance comparisons (in ANY language) when the duration is on the order of minutes.
Re:Clojure, Faster
by aphyr on Friday January 08, 2021 @05:15PM
(#25691232)
Criterium (the benchmarking library used here) uses multiple runs to obtain tighter bounds on amortized performance, as well as techniques to account for the effects of garbage collection and JIT compilation. See https://github.com/hugoduncan/criterium for a brief overview, as well as links to the pitfalls and statistical techniques involved in JVM benchmarking.
Re:Clojure, Faster
by bachmeier on Friday January 08, 2021 @05:55PM
(#25691597)
How often does it matter? For that matter, Java's not necessarily the first thing anyone reaches for when they have to push a piece of hardware to its limits.
Re:Clojure, Faster
by ithrow on Friday January 08, 2021 @03:59PM
(#25690426)
If you have to use java arrays, collections, methods and type hinting everywhere while also dismissing/avoiding more than half of the language to meet performance requirements (which is needed regularly in Clojure cause it's so slow), why not just write the thing in Java? Even Javascript will be much faster and your code won't get ugly that fast as in Clojure when you need performance.
You start by writing high-level, idiomatic code, because that's the cheapest to write. If that isn't giving you the performance you need, then you take steps to incrementally improve the performance without making any radical changes. If that still doesn't get the performance where you need it to be, or you find that you really are having to put the code through a mangler to get there, then, and only then, do you pay the cost of rewriting those components in a lower-level language.
IOW, don't take this list as a list of things that you should always do all the time. Take it as a list of specific things to help with solving specific problems, when you have those problems.
Re:Clojure, Faster
by tombert on Friday January 08, 2021 @05:03PM
(#25691116)
Because a majority of the time, the idiomatic Clojure will be fast enough.
I wouldn't consider Clojure to be a "slow" language, most of the time it'll perform pretty well, and has really great utilities for doing multithreaded code (which can also increase performance).
By using Java, you sacrifice a lot of the cool lispey features that you get with Clojure, and as long as it's possible to optimize Clojure in the parts that need to be optimized, then I think the language is worth it; depending on the context, you might even be able to write a macro to do these optimizations automatically while keeping the code somewhat idiomatic (though of course one needs to be careful with macros).
Re:Clojure, Faster
by agumonkey on Friday January 08, 2021 @04:27PM
(#25690748)
It's a very common thing actually. inline asm in C (you could argue just don't write C after all). Native modules in whatever slow dynamic language used in the mainstream.
I suppose the main value of the language is still too important to not use it, and only brings a different idiom on the small and hot spot where it's clearly an unavoidable move to win big.
The main problem is that some of these optimizations are very brittle in combination with one another and require a good deal of care to maintain their effectiveness. A person unfamiliar with your codebase who just marches in and starts writing idiomatic Clojure can inadvertently deoptimize large swaths of your codebase.
This is a problem that afflicts a lot of languages that have a wide variance in performance between idiomatic-but-slow and idiomatic-but-fast (or close to idiomatic) variants of the same code. Haskell is another good example of this, which is perhaps even more extreme in that very fast Haskell code can look perfectly idiomatic, but an otherwise innocent-looking change can crash everything to the ground.
That variance is always going to exist to some degree in every language, but if you find that you need to always or almost always be on the latter side of that divide to meet your performance targets, maintenance and knowledge transfer is going to be a huge pain. You might want to consider rewriting it in a different language that shifts the variance further down field so that you're back on the easy side of that divide for most of your codebase.
> keeping track of performance regressions using CI is not hard
It's actually kind of hard on the JVM. Again, if you have only a few hotspots then it's easy enough to set up some tests, but if you really need coverage over most of your codebase, it can get tricky, especially because these optimizations can combine in nonlocal ways so just exercising one function in one context doesn't guarantee the same performance in another. When taking into account warming up JIT compilation, benchmarks can become a real time suck. Criterium runs take a long time. You can't pop them like candy in the same way you can with unit tests, otherwise you can easily face hour+ CI runs.
The usual way I've seen this work successfully is if you get lucky and your project has very few overall code paths that you can test with e.g. integration tests and you can catch essentially all perf regressions there.
> requires the team to master a new language that isn't the first choice for other code in the project
I think you really need to have a good grasp of Java to write Clojure well (or you'll develop it as you write more Clojure). Both because the ecosystem makes liberal use of Java libraries and because otherwise none of the stack traces or even the optimization decisions listed in this article will really make sense. Clojure doesn't do much to hide Java from you (which has its own set of pros and cons).
My experience is pretty much the opposite. Java programming experience is not needed. Of course you'll develop your understanding and experience of the JVM platform and some Java APIs as a user over time but that's still far from being comfortable writing Java code. I'll concede that optimizing with annotations is helped by Java experience, especially is it includes Java perf work.
Edit: So I just ran the author's (alength (long-array 5)) (with the indirection to foil type inference) and got 10µs mean execution time in criterium. By contrast here's some roughly equivalent python:
More or less exactly the same with a plain python list. So clojure's type specialized function manages to be 300 times slower than the generic version in freaking *python*. In fairness "count" is better if still terrible (4x slower than python).Inside the JVM, there are several different types of arrays: bytes, floats, objects, etc. Clojure.lang.RT includes methods for getting the length of all of these types--for instance, here's the method for the length of arrays of longs:
https://github.com/clojure/clojure/blob/master/src/jvm/cloju...
The clojure.core/alength function calls clojure.lang.RT/alength, but that callsite is polymorphic: the Clojure compiler doesn't know which of the RT functions to emit a call for, because it needs the type signature. If the type is unavailable at compile time, the Clojure compiler emits reflective code which inspects the type of the reference, determines which specific clojure.lang.RT/alength implementation to dispatch to, then executes that call dynamically. That's the slow part!
https://github.com/clojure/clojure/blob/clojure-1.10.1/src/c...
Just like the article says, if you include a type hint, the compiler can emit an invokestatic call directly to, say, clojure.lang.RT.alength(long[]), and skip all the reflection.
But it can't need any less reflection than "count" needs, and "count" is 100x faster. Surely whatever alength is doing in the non-type hinted case can't be... ideal?
> If the type is unavailable at compile time, the Clojure compiler emits reflective code which inspects the type of the reference, determines which specific clojure.lang.RT/alength implementation to dispatch to, then executes that call dynamically. That's the slow part!
But where do all those clock cycles go? Doing runtime type inspection and then dispatching to one of 9 static functions based on that doesn't feel like it ought to be anywhere near that terrible (especially in a JIT'ed language).
Er, to be clear, Clojure's `alength` is a generic function--you're not measuring type-specialized behavior. The type-specialized `alength` call clocks in at around ~4 ns/call, as the article notes.
And it is not at all obvious to someone not intimately familiar with the JVM/Clojure that the element type would in any way matter for working out the length of an array. Indeed as far as I'm aware the OpenJVM memory representation for arrays has a length field at the same offset that's independent of the array element type, correct?
So that alength (in the typical case) is in fact 300x slower than len in python and 100x than count in clojure does not seem something that would be intuitively obvious to most people (including clojure programers).
https://clojure.org/reference/compilation#directlinking
This assumes that no code does some kind of var redefing, which is a pretty safe assumption to do.
To be honest, I find (JVM) Clojure is pretty fast, as mentioned elsewhere in the comments. Once my program has gotten off the ground -- most of the time my biggest wish is for faster startup time, and lately Babashka (an interpreted Clojure implemented with GraalVM, which starts more or less instantly) is my go-to for small, short programs I run often ... "scripts", I guess, though Clojure is expressive to do stuff in Babashka that I wouldn't dream of doing in Bash.
This can be detected by compiling a codebase with `unchecked-math :warn-on-boxed`.
Unfortunately many libs will emit boxed math warnings, so it isn't particularly easy to fail a CI build on this basis.
Still one can run the checks from time to time, and fix anything that can be fixed.
Where did you get that info? Never saw anything like that.
You start by writing high-level, idiomatic code, because that's the cheapest to write. If that isn't giving you the performance you need, then you take steps to incrementally improve the performance without making any radical changes. If that still doesn't get the performance where you need it to be, or you find that you really are having to put the code through a mangler to get there, then, and only then, do you pay the cost of rewriting those components in a lower-level language.
IOW, don't take this list as a list of things that you should always do all the time. Take it as a list of specific things to help with solving specific problems, when you have those problems.
I wouldn't consider Clojure to be a "slow" language, most of the time it'll perform pretty well, and has really great utilities for doing multithreaded code (which can also increase performance).
By using Java, you sacrifice a lot of the cool lispey features that you get with Clojure, and as long as it's possible to optimize Clojure in the parts that need to be optimized, then I think the language is worth it; depending on the context, you might even be able to write a macro to do these optimizations automatically while keeping the code somewhat idiomatic (though of course one needs to be careful with macros).
I suppose the main value of the language is still too important to not use it, and only brings a different idiom on the small and hot spot where it's clearly an unavoidable move to win big.
Clojure is not "so" slow - it's built expertly on top of the world's most advanced virtual machine.
Languages like PHP, Ruby, Python tend to lack a default VM that features GC or multithreading of such caliber.