2
votes

Assuming I have the following Clojure code:

(defn foo ^double []
  (- 
    (* 123.31
     (+ 4 5 6 (Math/sin 34.2))
     123.31) 
    123))

Will gen-class produce byte code equivalent to compiling the following java code:

public static double foo(){
   return (123.31 * (4 + 5 + 6 + Math.sin(34.2)) * 123.31) - 123;
}

Or in other words can I user Clojure as a very convenient DSL to produce efficient dynamic byte code?

Edit:

Ok, I did some test to illustrate my issue:

Here is java version:

public class FooTest {

  public static double foo(double a, double b, double c){
    return (a * (b + c + (b*c) + Math.sin(a)) * Math.log(b)) - b;
  }

  public static long benchmark(){
    long start = System.currentTimeMillis();
    for (double i = 0; i < 100000000.0; i++) { // 100 mln
      double r = foo(i, i+1, i+2);
    }
    long end = System.currentTimeMillis();
    return (end-start);
  }

  public static void main(String[] args) {
    System.out.println("Time took: "+benchmark());
  }
}

This produces the output: Time took: 39200

The clojure 'equivalent':

(defn foo ^double 
  (^double [a b c]
  (- 
    (* a
     (+ b c (* b c) (Math/sin a))
     (Math/log b)) 
    b)))

(time
  (loop [i 0.0] 
    (when (< i 100000000)   
      (foo i (+ i 1) (+ i 2))
      (recur (inc i)))))

That produces: "Elapsed time: 121242.902 msecs"

Which is 3 times slower.

Now my rephrased question is: How can I structure/hint clojure code so it avoids functions calls in code which is effectively primitive maths operations?

Edit2:

I have changed the test so it uses unchecked primitive maths operators:

(defn foo ^double 
  (^double [a b c]
  (binding [*unchecked-math* true] 
    (- 
      (* a
       (+ b c (* b c) (Math/sin a))
       (Math/log b)) 
      b))))

"Elapsed time: 64386.187 msecs" So it is almost 2 times better but still 1.6 times the java version.

2

2 Answers

2
votes

There is a little more to it than just the Clojure compiler because the JVM and hotspot JIT get to optimize the code as well. The Clojure compiler produces primitive math opps when all the values are primitive and any variables have primitive type hints. after this the Hotspot optimizer does the in-lining once the code is running on the JVM.

ps: using or not useing gen-class makes no difference in this process. All Clojure code is compiled and run the same way, except gen-class causes a file containing the byte-code to be created as well

0
votes

Ok, I finally got identical performance of Clojure to java. Three things needed change:

  1. Proper hinting of the function arguments (previously I hinted return value not arguments to the function)
  2. I moved binding out of the function body.
  3. Using unchecked math operations in the micro-benchmark helper as well

The resulting code is:

(binding [*unchecked-math* true]   
  (defn foo ^double [^double a ^double b ^double c]  
    (- 
      (* a
         (+ b c (* b c) (Math/sin a))
         (Math/log b)) 
      b)))

(binding [*unchecked-math* true]   
  (time
    (loop [i (double 0.0)] 
      (when (< i 100000000)   
        (foo i (+ i 1) (+ i 2))
        (recur (inc i))))))