Skip to content
Performance: Transients and Reducers
step 1/7

Reading — step 1 of 7

Learn

~3 min readcore.async, Java Interop, Performance

Clojure's persistent data structures are fast — but for hot paths, transients and reducers can give significant speedups. "Joy of Clojure" covers these as the performance toolbox.

Transients — local mutability

A transient is a temporarily-mutable version of a Clojure persistent collection. You build it imperatively, then convert back to persistent:

(defn build-vec [n]
    (loop [i 0
           v (transient [])]
        (if (< i n)
            (recur (inc i) (conj! v i))
            (persistent! v))))

(build-vec 5)        ;; [0 1 2 3 4]
  • (transient coll) — convert persistent to transient
  • conj!, assoc!, disj!, dissoc!, pop! — mutating operations
  • (persistent! tcoll) — convert back; you can't use transient after this

Rules:

  • Transients are LOCAL — don't share between threads
  • Don't read intermediate values — only the final persistent! result is correct
  • Only the bang-suffixed variants of operations are valid

Performance: 2-3x faster for building large collections. Standard library uses transients internally for into, mapv, etc.

When to reach for transients

  • Building a large vector / map / set in a loop
  • Standard library's into is already transient-optimized
  • For one-off persistent operations, the difference is negligible
;; Standard library uses transients:
(into {} (map (fn [n] [n (* n n)]) (range 100)))

Reducers — parallel reductions

The clojure.core.reducers namespace provides r/map, r/filter, r/fold that don't allocate intermediate lists:

(require '[clojure.core.reducers :as r])

(reduce + 0 (r/map #(* % 2) (range 10000)))
;; Same result as without r/, but no intermediate seq

r/fold further enables parallel reduction:

(r/fold + (vec (range 100000)))
;; Splits work across cores via fork/join

fold only works on foldable collections (vectors and some others). Lists don't support efficient splitting.

Transducers — composable transformations

(def xform (comp (map inc) (filter even?) (take 5)))

(into [] xform (range 100))
;; [2 4 6 8 10]

A transducer is a stateless function transformation independent of the input source. Same xform works on:

  • Eager: into, transduce
  • Lazy: sequence
  • Channels: chan with xform argument

Transducers don't allocate intermediate seqs. Pure performance win for long pipelines.

Primitive math

;; Boxed (slow):
(defn sum [n]
    (loop [i 0 acc 0]
        (if (< i n) (recur (inc i) (+ acc i)) acc)))

;; Primitive (fast):
(defn sum [^long n]
    (loop [i (long 0) acc (long 0)]
        (if (< i n) (recur (inc i) (+ acc i)) acc)))

Clojure boxes longs by default. Type hints (^long) and (long ...) casts force primitive arithmetic. Crucial for numeric-heavy hot paths.

There's a 4-arity arithmetic limit — adding more than 4 longs at once boxes:

(+ a b c d)        ;; primitive (4 args)
(+ a b c d e)      ;; boxed (5+ args)

Profiling

  • Criterium — micro-benchmarking library; handles JVM warmup
  • VisualVM, YourKit — JVM profilers
  • (time ...) — built-in timing macro for ad-hoc checks
(time (reduce + 0 (range 1e6)))
;; "Elapsed time: 12.3 msecs"

When to optimize

Most Clojure code doesn't need any of this. Reach for it when:

  • Profiling identifies a real hot path
  • Numeric / collection-building work dominates
  • You're writing a library (10x speedups matter to all consumers)

For application code, idiomatic Clojure is fast enough. Fancy tricks usually obscure intent.

Common mistakes

  • Sharing transients across threads — they're not thread-safe. Local mutation only.
  • Using assoc after assoc! — once you've gone transient, use bang variants throughout.
  • r/fold on a list — only works on foldable collections (vectors). Convert first.
  • Unboxed math without hints — Clojure boxes by default. Add ^long, ^double in numeric hot paths.
  • Profiling without JIT warmup — use Criterium for accurate measurements; raw time is biased by warmup.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…