spork
spork copied to clipboard
Faster get-in
So the default clojure.core nested operations like get-in can be optimized if we have a path of literals known a-priori. We can compile that down to a sequence of lookups (preferably optimized .get ops on a java.util.Map or .valAt, or for broad generality, clojure.core/get). This avoids creating a vector at runtime, reducing over the vector, etc.
We can't do that in the case where values are passed in at runtime and we don't know the compile-time path (e.g. we have a path vector that includes symbols).
We can still kick clojure.core/get-in to the curb if we replace the default implementation with one that's slightly restrictive on types, in this case java.util.Map:
(defmacro get-in-map [m ks]
(if (seq ks)
`(let [^java.util.Map m# ~m]
(if-let [res# (.get ^java.util.Map m# ~(first ks))]
(get-in-map res# ~(rest ks))))
`~m))
The only restriction is that we provide a literal collection rather than a symbol for the path, but it works with normal clojure idioms.
user> (let [m {:a {:b {:c 3}}} x :c] (time (dotimes [i 1000000] (get-in m [:a :b x]))))
"Elapsed time: 116.3892 msecs"
user> (let [m {:a {:b {:c 3}}} x :c] (time (dotimes [i 1000000] (get-in-map m [:a :b x]))))
"Elapsed time: 28.9272 msecs"
user> (let [m {:a {:b {:c 3}}} x :c] (get-in-map m [:a :b x]))
3
I dug this up since I overlooked get-in
before when doing deep-assoc and friends, and I did not at the time know of how costly clojure.core/get is.
Oh, and unlike clojure.core/get-in, this variant is naturally short-circuiting if the path leads to nothing, so we get a bit more optimization (potentially a lot if there are deeply nested lookups):
user> (let [m {:a {:b {:c 3}}} x :d] (time (dotimes [i 1000000] (get-in m [:a x :c]))))
"Elapsed time: 119.1608 msecs"
user> (let [m {:a {:b {:c 3}}} x :d] (time (dotimes [i 1000000] (get-in-map m [:a x :c]))))
"Elapsed time: 19.3677 msecs"