~ / track B / clojure advanced
Type hints and performance
AdvancedClojure is dynamically typed, but on the JVM it compiles to typed bytecode.
The compiler will use Java's Object for anything ambiguous, which forces
reflection at every method call. Type hints are metadata that tells the
compiler "this is a String," "this is a long" — eliminating reflection
and unlocking primitive math.
Most code does not need them. Hot loops, low-level math, and Java interop do.
See reflection happen
(set! *warn-on-reflection* true)
(defn first-char [s]
(.charAt s 0))
;; Reflection warning - call to method charAt can't be resolvedThe warning fires because the compiler doesn't know s is a String — at
the call site, it generates reflective dispatch. Hint it:
(defn first-char [^String s]
(.charAt s 0))
;; (no warning, direct invokevirtual)^String is metadata on the parameter. The compiler now compiles
(.charAt s 0) to a direct method call on String. Reflective dispatch
can be one to two orders of magnitude slower than a direct call on hot
paths; on cold paths the difference is invisible.
Hinting return types
For interop calls whose return type the compiler can't infer:
(defn ^java.util.Date now [] (java.util.Date.))Without the return hint, callers that do .getTime on the result will
reflect.
Primitive math
Clojure uses boxed Long and Double by default. For tight numeric
loops, you can use unboxed primitives:
(defn sum-to ^long [^long n]
(loop [i 0 acc 0]
(if (== i n)
acc
(recur (inc i) (+ acc i)))))^long on a parameter or the arg vector means primitive long —
the return-type hint goes on the arg vector, not the function name. The
body must stay in primitive land — adding a BigInt or doing (* a 1.5)
re-boxes. With primitives plus loopclojure.core/looplet plus a recursion target for recur.view on clojuredocs →/recurclojure.core/recurTail-recursive call to the enclosing loop or fn.view on clojuredocs →, numeric kernels can get
within a few percent of hand-written Java.
Limit: a primitive-arg function supports up to 4 primitive args, and
they must be long or double. For more, pack into arrays or records.
*warn-on-reflection*
Always turn this on for performance-sensitive namespaces:
(ns myapp.hot-path
(:require ...))
(set! *warn-on-reflection* true)Compile-time warnings flag every reflective call. The rule: zero warnings in the namespaces you care about.
You can also enable it project-wide by passing a JVM flag — for example, a
:dev alias in deps.edn:
;; deps.edn
{:aliases
{:dev {:jvm-opts ["-Dclojure.compiler.warn-on-reflection=true"]}}}Then clj -M:dev compiles every namespace with the warning on.
defclojure.core/defBind a name to a value in the current namespace.view on clojuredocs → of expensive values
(def regex (re-pattern "...")) ;; computed at load, fine
(def big (delay (compute-big))) ;; computed at first use; see [[futures-promises-delay]]Avoid recomputing constants. Don't inline (re-pattern ...) inside a hot
function — it'll recompile the regex every call.
transient for batch construction
Persistent collections are O(log32 n) per write — fast, but not free. If you build a collection in one place and never share intermediates, use a transient:
(defn build-set [n]
(persistent!
(reduce conj! (transient #{}) (range n))))transient!, conj!, assoc!, dissoc!, pop! are O(1) amortized for
local construction. Always end with persistent! to return to immutable
land. Don't share transients across threads.
Internally, [[transducers]] use this pattern to build results without intermediate allocation.
Arrays for true hot paths
Java arrays are unboxed and fast:
(let [^longs xs (long-array 1000)]
(dotimes [i (alength xs)]
(aset xs i (* i 2))))aset, aget, alength, amap, areduce are the array primitives.
Reach for them when you need numerical kernels or memory-tight buffers —
and not before.
definline, proxy, gen-class — niche
definlineemits an inline function — cheaper than a regular call, but fragile. Use rarely.proxycreates a Java subclass at runtime — slow, useful for tests.gen-classahead-of-time compiles a real Java class with methods you can call from Java. Needed when Java needs to call back into Clojure.
A pragmatic checklist
Before adding hints, measure. Most Clojure code doesn't need them, and adding them everywhere clutters reading. The rule:
- Profile (Criterium, async-profiler).
- Find the hot path.
- Turn on
*warn-on-reflection*for that namespace. - Hint to clear the warnings.
- If still slow, try primitive args / transients / arrays.
- Re-measure.
Try it (informational)
Reflection penalties are a JVM-only phenomenon — the in-browser SCI REPL
runs on JavaScript, where (.length s) would fail anyway (JS strings
expose length as a property, not a method). The snippet below describes
the JVM behavior:
;; Without a hint, the JVM compiler can't resolve .length on s and falls
;; back to reflection at runtime — slower per-call.
(defn count-chars [s]
(.length s))
;; With a hint, the compiler emits a direct String.length() call.
(defn count-chars-fast [^String s]
(.length s))To see something equivalent in the browser, use CLJS property access:
Real-world
| Where speed matters | What to do |
|---|---|
| Web request handlers | Usually I/O-bound; rarely needs hints. Focus on connection pools and JSON parser choice |
| JSON / EDN deserialization | Use jsonista (Jackson-backed) — its internal hot path is fully hinted |
| Numeric simulation, finance, ML preprocessing | ^long / ^double args, loopclojure.core/looplet plus a recursion target for recur.view on clojuredocs →/recurclojure.core/recurTail-recursive call to the enclosing loop or fn.view on clojuredocs →, sometimes arrays |
| Datomic / XTDB indexing layer | Hand-tuned with arrays and primitives |
nippy serialization | Uses ^byte arrays throughout for compact encoding |
| ClojureScript hot rendering paths | Different rules — see Reagent's ^js hint and goog.string |
| Benchmarking tooling | criterium for micro-benchmarks; JMH for cross-JVM comparisons |
Check yourself
? quiz
`*warn-on-reflection*` shows a warning on `(.length s)`. Adding which hint silences it?
Exercise
Take this function:
(defn dot [xs ys]
(reduce + (map * xs ys)))- Run it on two long vectors and benchmark with
(time (dot xs ys)). - Rewrite it as a primitive-friendly loopclojure.core/looplet plus a recursion target for recur.view on clojuredocs →/recurclojure.core/recurTail-recursive call to the enclosing loop or fn.view on clojuredocs → over
^longsarrays, with^longaccumulator and(aget). - Benchmark again. Where does the speedup come from — fewer allocations, primitive ops, or removed reflection?