Advent of Clojure: NFS 2026
This is a sequel to my AoC: NFS blogpost from 2023. Here I’ll further explore some ways to improve the performance of Clojure code, based on my AoC 2025 solutions.
Using a correct algorithm is still the best way to improve the performance by a huge margin. In that category, but with a smaller gains, is also “don’t do unneeded stuff” (e.g. creating a vector of some elements when all you’re interested in is the count of those elements).
All stuff mentioned in the original post is still valid, and here will be some additional things to be aware of. This time I’ll keep it brief.
clj-async-profiler
In the original post I mentioned Criterium. This time it is time to mention another very useful profiling tool: clj-async-profiler.
Its documentation website gives tips on how to use it and how to read, explore and understand the flamegraphs, etc.
Thanks to it, I managed to find various time-wastings in places where I wouldn’t expect.
Reflections
In the original post I mentioned (set! *unchecked-math* :warn-on-boxed).
Another useful setting is (set! *warn-on-reflection* true), which shows warnings for places where reflection is needed to resolve method calls or field access.
For example:
(defn my-func [x y]
(Math/floorDiv x y)))
; Reflection warning, (...)/user.clj:96:5 -
; call to static method floorDiv on java.lang.Math can't be resolved
; (argument types: unknown, unknown).
The solution is to type-hint the function arguments:
(defn my-func [^long x ^long y]
(Math/floorDiv x y))
It is not needed to fix this warnings everywhere, but in some tight loops might give some surprisingly large benefits.
Loop variables
(defn some-func [xs]
(let [n (count xs)]
(loop [i 0
acc 0]
(if (>= i n)
acc
(recur (inc i)
(+ acc (xs i)))))))
The loop variables i and acc start as long, but the result of (xs i) is a boxed number, so after the first iteration the acc variable becomes boxed too.
This is also caught by the *warn-on-reflection*:
recur arg for primitive local: acc is not matching primitive, had: Object, needed: long
Auto-boxing loop arg: acc
The solution is to convert the result back to long: (long (xs i)).
The significance of this improvement depends on the speed of the rest of the loop.
In the above case, where loop is quite simple and fast, the improvement is 25%.
(>= start 1 0 end)
For one task there was a condition that start must be positive and end must be zero or less.
I wrote it in a single >= as (>= start 1 0 end).
The problem? The >= function for more than two arguments uses variadic arguments ([x y & more]), which causes a slowdown compared to the version with just two arguments.
The solution: use (and (pos? start) (<= end 0)).
This way the hot loop this was in gained a 4x performance boost.
Deeply nested for
In one task I had a for comprehension which looked something like this:
(for [x xs
y ys
z zs
w ws]
[x y z w]) ; real example was more convoluted than this
A bit faster approach was to use doseq instead:
(let [result (atom (transient []))]
(doseq [x xs
y ys
z zs
w ws]
(swap! result conj! [x y z w]))
(persistent! @result))
I don’t expect this to work everywhere nor to produce meaningful differences every time, but in this specific case it was worth it.
Java stuff
Long/toString: If you know your input is a number, it is faster to convert it to string by using the specializedLong/toStringmethod than go through the more generalstrfunction..charAt: Faster to do(.charAt s idx)than(get s idx), and much faster than(first s)for the first character.long-array: Sometimes it makes sense to use Java arrays instead of Clojure’s vectors.
Conclusion
That’s it for this time.
Now we’re ready for pointless optimizations for AoC 2026, coming out in few weeks time.