Skip to content

Direct Linking

Justin Conklin edited this page Aug 17, 2020 · 2 revisions

Clojure's direct linking feature is not just limited to AOT-compiled code; it works the same with code loaded in a REPL, for example. You can verify this by viewing the dynamically compiled bytecode with something like no.disassemble.

We can measure the difference in performance with a simple experiment. First, a couple of vars:

(ns demo.core)

(defn foo ^long [^long a ^long b]
  (unchecked-add a b))

(defn bar ^long [^long a ^long b]
  (foo a b))

... and our benchmark data:

{:benchmarks
 [{:ns demo.core :fn [foo bar] :args [:long :long]}]

 :states
 {:long (fn [] (rand-int 10000000))}

 :options
 {:jmh/default {:output-time-unit :us}
  :dlink {:fork {:jvm {:append-args ["-Dclojure.compiler.direct-linking=true"]}}}}}

Now we'll run some tests with lein-jmh. First, without direct linking:

lein jmh '{:only [:fn :score], :format :table}'
# => :benchmark     :score
#    -------------  ---------------
#    demo.core/foo  388.867  ops/us
#    demo.core/bar  328.787  ops/us

We see above that bar takes a small but measurable hit in throughput due to the cost of dereferencing the foo var on each invocation.

Now to enable direct linking for the forks by merging our :dlink option group:

lein jmh '{:only [:fn :score], :format :table, :type :dlink}'
# => :benchmark     :score
#    -------------  ---------------
#    demo.core/foo  390.605  ops/us
#    demo.core/bar  391.803  ops/us

We see the difference when the var overhead is taken out of the picture: both are virtually identical. Just to reiterate, neither of our functions here have been AOT-compiled.

Also note, jmh-clojure explicitly dereferences benchmark Vars when they are required in a JMH subprocess, so there is no var-indirection overhead there either.

Clone this wiki locally