Specter on the JVM, babashka and Jolt

The same Specter code runs on all three hosts, and gives the same answers. Upstream Specter tests the JVM and babashka, and Malai runs that suite, plus its own tests, on Jolt and on the JVM. What differs is how each host runs it, and that decides how fast a path is and what it costs to start.

Side by side

JVMbabashkaJolt (Malai)
What runs your ClojureClojure compiles it to JVM bytecodeSCI interprets it form by form, inside a GraalVM native-image binaryJolt compiles it to native code through Chez Scheme
Which Spectercom.rpl/specter from Clojars, with riddley and two small compiled Java classesUpstream Specter from source; its #?(:bb ...) branches swap riddley, reducers and the Java classes for plain ClojureMalai, a fork of Specter with #?(:jolt ...) branches beside the :bb ones
Inline path cachingYes: each call site compiles its path on the first call (an eval of a fn over the path's locals)No: under :bb, path is comp-paths, so every call rebuilds its pathYes: a static path is cached on its first call, a path over locals compiles after 256 calls (how and why)
Seeing locals inside nested pathsriddley's macroexpand-all, which keeps &envNot needed: nothing is cachedMacro calls in a path stay unexpanded and expand inside the compiled fn, where the locals are its parameters
StartupA JVM start, then Clojure'sInstant: a native binaryA native binary; a jolt build binary caches paths too
At the REPLA cached path keeps what it saw on its first callNothing goes staleAs on the JVM, plus malai.core/clear-path-cache!

Timings

jolt bench on Jolt, the same benchmark run by bb on babashka, and Specter 1.1.4 on the JVM with the same data and paths. An Apple M1 Pro, µs per call, after warm-up (Jolt 0.8.27, babashka 1.13.220, Clojure 1.12.3):

JVMbabashkaJolt (Malai)
static path [MAP-VALS ALL :y ALL even?], transform2.5821.486.84
dynamic path [MAP-VALS ALL (keypath k)], select0.8115.321.91
hand-written update/mapv, same result1.061.951.70

Caching is most of the gap between babashka and Jolt: babashka rebuilds the path on every call, as Malai 0.1 did on Jolt (18.75 µs and 10.37 µs). What stays between Jolt and the JVM is how fast each host runs the navigators themselves.

Which to use

  • On the JVM, use com.rpl/specter from Clojars. It's the fastest of the three once warm, and Malai adds nothing there.
  • In a babashka script, Specter loads from source and works as is. Paths run uncached, which is fine for scripts that touch them a few hundred times.
  • On Jolt, use malai. It's the only one of the three that loads there, and it caches paths, so a loop or a long REPL session gets close to a path compiled by hand.

Never put Malai and com.rpl/specter in one project: they provide the same namespaces, and whichever loads first wins.