[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)