For some time I had a suspicion that in Clojure we have a fundamental 
problem with efficient math and today I've tried to test it.

Let's say we have a pretty simple function:

(ns testapp.core
  (:require
   [criterium.core :as cr]
   [primitive-math :as p]
   [no.disassemble :as nd]))

(set! *warn-on-reflection* true)

(defn ^double test1 [a b]
  (let [^doubles a a
        ^doubles b b]
    (areduce a i ret (double 0)
             (p/+ ret
                  (let [a-x (aget a i)
                        b-x (aget b i)]
                    (if (p/> a-x b-x)
                      (p/- a-x b-x)
                      (p/- b-x a-x)))))))

Let's test it's performance (all tests were performed in Clojure 1.5.1 on 
top of latest JVM7 and Sandy Bridge i5):

(defn bench1 []
  (let [a (double-array (range 1 100000))
        b (double-array (range 100000 1 -1))]
    (cr/quick-bench (test1 a b))))

Result:

             Execution time mean : 223.076094 µs
    Execution time std-deviation : 21.413850 µs

It's around 1 nanosecond per double operation, so it looks adequate to me. 
Disassemble looks fine to me, too.

Now let's refactor the code a little, that compare-and-diff piece looks 
generally useful:

(defn ^double abs-diff [^double x ^double y]
  (if (p/> x y) (p/- x y) (p/- y x)))

(defn ^double test2 [a b]
  (let [^doubles a a
        ^doubles b b]
    (areduce a i ret (double 0)
             (p/+ ret (double (abs-diff (aget a i) (aget b i)))))))

(defn bench2 []
  (let [a (double-array (range 1 100000))
        b (double-array (range 100000 1 -1))]
    (cr/quick-bench (test2 a b))))

Suddenly:

             Execution time mean : 877.690451 µs
    Execution time std-deviation : 15.281713 µs

Why is it so? Disassemble reveals a few inefficiencies here and there. Here 
is an interesting bit of test2's invoke:

    32  lload 6 [i]
    34  lconst_1
    35  ladd
    36  dload 8 [ret]
    38  getstatic testapp.core$test2.const__5 : clojure.lang.Var [57]
    41  invokevirtual clojure.lang.Var.getRawRoot() : java.lang.Object [76]
    44  checkcast clojure.lang.IFn$DDO [78]
    47  aload_3 [a]
    48  checkcast double[] [72]
    51  lload 6 [i]
    53  invokestatic clojure.lang.RT.intCast(long) : int [82]
    56  daload
    57  aload 4 [b]
    59  checkcast double[] [72]
    62  lload 6 [i]
    64  invokestatic clojure.lang.RT.intCast(long) : int [82]
    67  daload
    68  invokeinterface clojure.lang.IFn$DDO.invokePrim(double, double) : 
java.lang.Object [86] [nargs: 5]
    73  invokestatic clojure.lang.RT.doubleCast(java.lang.Object) : double 
[90]
    76  invokestatic primitive_math.Primitives.add(double, double) : double 
[96]
    79  dstore 8 [ret]
    81  lstore 6 [i]
    83  goto 19
    86  goto 95

If I understand this correctly, in this piece of code we have following 
problems:
1) there are some unnecessary casts from long to int. Probably this is 
caused by areduce macro (it uses "0" as a counter and it's long in Clojure, 
but arrays are indexed with ints). Replacing it with a loop doesn't help 
much, maybe JIT optimizes this already.
2) every call to abs-diff goes through Var dereference.
3) abs-diff returns Object despite the ^double return annotation (this is 
why (double ...) cast is needed there to avoid reflection warning). 
Therefore we can't avoid boxing-unboxing here.

Is there a way to use small, reusable, composable pieces in math-heavy hot 
loops? We can use macros instead of functions and compile everything into 
one big method. This is the way HipHip(array) of Prismatic works. The 
problem here is that their examples are tiny and bigger stuff like 
decompositions will result in very large compiled methods. This is a bad 
practice in Java-land, because in general JVM doesn't like such methods, 
skipping their inlining and delaying optimization. Moreover, macros have 
composability problems, too. Therefore, macros are not the ultimate 
solution here.

So, what's the best way to write beautiful AND performant numerical code in 
Clojure?

The full code for this email can be found 
here: https://gist.github.com/si14/10008500

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
--- 
You received this message because you are subscribed to the Google Groups 
"Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to clojure+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to