I am working with bit strings in clojure. My question is less about performance and more about how one should go about it (in particular see the last inlined version)
The tonnetz-array represents an adjacency matrix.
(set! *warn-on-reflection* true)
(set! *unchecked-math* true)
(def ^:private ^ints tonnetz-array
(int-array [952 1904 3808 3521 2947 1799 3598 3101 2107 119 238 476]))
get-tonnetz-connected can be thought of as a graph lookup.
(defn- get-tonnetz-connected
"given packed binary notes representing a chord,
returns packed notes that mark all notes that can be reached in
the tonnetz graph. This function is used by tonnetz-connected? as
a way of building a map / traversing a graph at a far faster
rate than a graph search"
[packed]
(loop [mask (int 0)
^int bits packed]
(if (= bits 0)
mask
(let [low-bit (Integer/lowestOneBit bits)]
(recur
(bit-or mask (aget ^ints tonnetz-array
^int (Integer/numberOfTrailingZeros low-bit)))
(bit-xor bits low-bit))))))
this is the main function.
(defn tonnetz-connected?
"do the notes form a connected graph?"
[packed]
(let [low-bit (Integer/lowestOneBit packed)]
(loop [mask (aget ^ints tonnetz-array
(Integer/numberOfTrailingZeros low-bit))
unmatched (bit-xor packed low-bit)]
(if (= 0 unmatched)
true
(let [matches (bit-and mask unmatched)
masks (bit-or mask (get-tonnetz-connected matches))
next-unmatched (bit-xor matches unmatched)]
(if (= unmatched next-unmatched)
false
(recur masks next-unmatched)))))))
I have found that inlining has improved things a bit, and this thing is fast and it is fast enough, but I would like to understand how to properly get the most out of low level operations.
An inlined version where I have tried to make sure that ints are used all the way though.
(defn tonnetz-connected?-2
"do the notes form a connected graph?"
[packed]
(let [low-bit (int (Integer/lowestOneBit (int packed)))]
(loop [mask (int (aget ^ints tonnetz-array
(int (Integer/numberOfTrailingZeros low-bit))))
unmatched (int (bit-xor (int packed) low-bit))]
(if (= 0 unmatched)
true
(let [matches (int (bit-and mask unmatched))
masks (loop [mask (int mask)
bits (int matches)]
(if (= bits (int 0))
mask
(let [low-bit (int (Integer/lowestOneBit bits))]
(recur
(int (bit-or mask (aget ^ints tonnetz-array
^int (int (Integer/numberOfTrailingZeros low-bit)))))
(int (bit-xor bits low-bit))))))
next-unmatched (int (bit-xor matches unmatched))]
(if (= unmatched next-unmatched)
false
(recur (int masks) (int next-unmatched))))))))
A dirty test function
(defn testit [n f]
(let [xs (repeatedly n #(+ 1 (rand-int 4094)))]
(time (do (doall (map f xs)) nil))))
generally I am averaging (ran each about 10 times):
clj-music.core> (testit 100000 tonnetz-connected?)
"Elapsed time: 77.175712 msecs"
and for the inlined version
clj-music.core> (testit 100000 tonnetz-connected?-2)
"Elapsed time: 61.413721 msecs"
So the performance has improved, but now I just have (int ...)
everywhere. This was mainly to silence warnings like:
core.clj:165 recur arg for primitive local: mask is not matching primitive, had: Object, needed: long
Auto-boxing loop arg: mask
Is this the correct way?
tonnetz-array
come from? A reference explaining the model and representation would be handy. \$\endgroup\$Long/numberOfTrailingZero
and not(int ...)
is about 77.ms without inlining. \$\endgroup\$