[HN Gopher] Clojure, Faster
___________________________________________________________________
Clojure, Faster
Author : elamje
Score : 101 points
Date : 2021-01-06 07:14 UTC (2 days ago)
(HTM) web link (tech.redplanetlabs.com)
(TXT) w3m dump (tech.redplanetlabs.com)
| vemv wrote:
| There's a better alternative to adding ^:const by hand, namely
| compiling production artifacts with _direct linking_ :
|
| 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.
| rcgorton wrote:
| 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.
| aphyr wrote:
| 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.
| ithrow wrote:
| 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.
| agumonkey wrote:
| 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.
| vemv wrote:
| > (which is needed regularly in Clojure cause it's so slow)
|
| 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.
| tombert wrote:
| 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).
| mumblemumble wrote:
| I imagine it's like any high(er) level language.
|
| 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._
| nojito wrote:
| Maintaining this type of code is impossible...but offers an
| excellent job guarantee if you convince your code reviewers to
| check it in.
| dwohnitmok wrote:
| 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.
| fulafel wrote:
| 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.
| dwohnitmok wrote:
| 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).
| vemv wrote:
| Number boxing deserves a mention as well: sometimes even code
| that might appear to be adequately type-hinted may be performing
| boxing under the hood.
|
| 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.
| patrec wrote:
| 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 10us 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).
| aphyr wrote:
| _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.
| gameswithgo wrote:
| 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.
| aphyr wrote:
| 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:
|
| 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.
| xedrac wrote:
| So just switching from (first v) to (nth v) offers a 5x speedup?
| I wonder why they opted for the lazy API for first.
| bachmeier wrote:
| 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.
___________________________________________________________________
(page generated 2021-01-08 23:00 UTC)