0

This is a follow up to @cgrand's answer to the question "Clojure Performance For Expensive Algorithms." I haven been studying it and trying to apply some of his techniques to my own experimental Clojure perf tuning.

One thing I am wondering about is the "ugly" "arrays swap trick"

      (set! curr prev)
      (set! prev bak)

How and why does this improve performance over the original approach? I suspect that Clojure arrays are sometimes not true Java primitive arrays? If necessary, please cite Clojure core source in your answer.

Community
  • 1
  • 1
noahlz
  • 10,202
  • 7
  • 56
  • 75

2 Answers2

0

As Chas mentions, loops with primitive hints are problematic. Clojure attempts to keep ints unboxed when you provide a hint, but it will (for the most part) silently fail when it can't honor the hints. Therefore he is forcing it to happen by creating a deftype with mutable fields and setting those inside the loop. It's a ugly hack, but gets around a few limitations in the compiler.

Timothy Baldridge
  • 10,455
  • 1
  • 44
  • 80
0

In fact, it has to do with object allocation. Here's the original algorithm, with annotations:

(defn my-lcs [^objects a1 ^objects a2]
  (first
    (let [n (inc (alength a1))]
      (areduce a1 i 
        ;; destructuring of the initial value 
        [max-len ^ints prev ^ints curr] 
        ;; initial value - a vector of [long int[] int[]]
        [0 (int-array n) (int-array n)] 
        ;; The return value: a vector with the prev and curr swapped positions.
        [(areduce a2 j max-len (unchecked-long max-len)  ;; 
           (let [match-len 
                 (if (.equals (aget a1 i) (aget a2 j))
                   (unchecked-inc (aget prev j))
                   0)]
             (aset curr (unchecked-inc j) match-len)
             (if (> match-len max-len)
               match-len
               max-len)))
         curr prev])))) ;; <= swaps prev and curr for the next iteration

As per the Java version, prev and curr are "reused" - a dynamic programming approach similar to what's described here. However, doing so requires allocating a new vector on every iteration, which is passed to the next reduction.

By placing prev and curr outside the areduce and making them ^:unsynchronized-mutable members of the enclosing IFn object, he avoids allocating a persitent vector on each iteration, and instead just pays the cost of boxing a long (possibly not even that).

So the "ugly" trick was not in a prior iteration of his Clojure code, but rather the Java version.

noahlz
  • 10,202
  • 7
  • 56
  • 75