Performance
Prerequisites: Java interop, Transducers
Idiomatic Clojure is fast enough for most work — it compiles to JVM bytecode and benefits from the JIT. When a hot path needs more, these are the tools, roughly in order of impact.
1. Avoid reflection
(set! *warn-on-reflection* true)
(defn slow-length [s] (.length s))
(defn fast-length [^String s] (.length s))
(println (slow-length "reflection") (fast-length "hinted"))
Reflection warning, C:\demo\main.clj:3:23 - reference to field length can't be resolved.
10 6
Without a type hint, the compiler can't know which .length to call,
so it looks it up by reflection on every call — often 10–100× slower.
Turn on *warn-on-reflection* (projects usually set it for all
namespaces) and add ^String, ^long, or class hints where warnings
appear.
2. Use the right data structure and algorithm
- Look up by key in maps and sets, not by scanning vectors.
- Use transducers or
reduceinstead of long chains of lazy sequences in hot loops. frequencies,group-by, andintoare already optimized — prefer them to hand-written loops.
3. Transients for building collections
(defn squares-persistent [n]
(loop [i 0 v []]
(if (< i n) (recur (inc i) (conj v (* i i))) v)))
(defn squares-transient [n]
(loop [i 0 v (transient [])]
(if (< i n) (recur (inc i) (conj! v (* i i))) (persistent! v))))
(println (= (squares-persistent 100000) (squares-transient 100000)))
(println (count (squares-transient 1000000)))
true
1000000
A transient is a temporarily mutable version of a collection, for
use inside one function: conj!, assoc!, then persistent! to get an
ordinary immutable value back. into and frequencies use them
internally.
4. Primitive math
(defn sum-to ^long [^long n]
(loop [i 0 acc 0]
(if (< i n) (recur (inc i) (+ acc i)) acc)))
(println (sum-to 10000000))
(println (unchecked-add Long/MAX_VALUE 1))
49999995000000
-9223372036854775808
^long and ^double hints on function arguments and return values let
loops run on primitives without boxing. unchecked-add and friends
skip overflow checks (and wrap around, as the second line shows) — use
only when you're sure.
Measuring
;; illustrative — timings vary
(require '[criterium.core :refer [quick-bench]])
(quick-bench (squares-transient 100000))
Evaluation count : 1242 in 6 samples of 207 calls.
Execution time mean : 487.312 µs
- Criterium benchmarks correctly (JIT warm-up, statistics); plain
(time ...)is fine for a rough idea. - clj-async-profiler produces flame graphs from a running REPL.
- JVM options matter: heap size (
-Xmx), GC choice, and-Dclojure.compiler.direct-linking=truefor production builds.
Start-up time
A JVM Clojure program takes around a second (or more, with many libraries) to start. Ahead-of-time compilation (AOT, done by uberjar builds) helps; for scripts, use Babashka; for instant-start binaries, compile with GraalVM native-image.