Is NodeJs faster than Clojure?


Question

I just started learning Clojure. One of the first things I noticed is that there are no loops. That's OK, I can recur. So let's look at this function (from Practical Clojure):

(defn add-up
  "Adds up numbers from 1 to n"
  ([n] (add-up n 0 0))
  ([n i sum] 
    (if (< n i)
      sum
      (recur n (+ 1 i) (+ i sum)))))

To achieve the same function in Javascript, we use a loop like so:

function addup (n) {
  var sum = 0;
  for(var i = n; i > 0; i--) {
    sum += i;
  }
  return sum;
}

When timed, the results look like:

input size: 10,000,000
clojure: 818 ms
nodejs: 160 ms

input size: 55,000,000
clojure: 4051 ms
nodejs: 754 ms

input size: 100,000,000
clojure: 7390 ms
nodejs: 1351 ms

I then proceeded to try the classic fib (after reading this):

in clojure:

(defn fib
  "Fib"
  [n]
  (if (<= n 1) 1
      (+ (fib (- n 1)) (fib (- n 2)))))

in js:

function fib (n) {
  if (n <= 1) return 1;
  return fib(n-1) + fib(n-2);
}

Again, the performance has quite some difference.

fib of 39
clojure: 9092 ms
nodejs: 3484 ms

fib of 40
clojure: 14728 ms
nodejs: 5615 ms

fib of 41
clojure: 23611 ms
nodejs: 9079 ms

Note I'm using (time (fib 40)) in clojure so it's ignoring the bootup time for JVM. These are ran on a MacBook Air (1.86 Ghz Intel Core 2 Duo).

So what's causing Clojure to be slow here? And why do people say that "Clojure is fast"?

Thanks in advance and Please, no flame wars.

1
31
6/2/2011 3:47:36 PM

Accepted Answer

(set! *unchecked-math* true)

(defn add-up ^long [^long n]
  (loop [n n i 0 sum 0]
    (if (< n i)
      sum
      (recur n (inc i) (+ i sum)))))

(defn fib ^long [^long n]
  (if (<= n 1) 1
      (+ (fib (dec n)) (fib (- n 2)))))

(comment
  ;; ~130ms
  (dotimes [_ 10]
    (time
     (add-up 1e8)))

  ;; ~1180ms
  (dotimes [_ 10]
    (time
     (fib 41)))
  )

All numbers from 2.66ghz i7 Macbook Pro OS X 10.7 JDK 7 64bit

As you can see Node.js is trounced. This is with 1.3.0 alphas, but you can achieve the same thing in 1.2.0 if you know what you're doing.

On my machine Node.js 0.4.8 for addup 1e8 was ~990ms, for fib 41 ~7600ms.

            Node.js  | Clojure
                     |
 add-up       990ms  |   130ms
                     |
 fib(41)     7600ms  |  1180ms
48
3/30/2015 10:34:11 AM

I'd actually expect Clojure to be significantly faster than Javascript if you optimise your code for performance.

Clojure will statically compile to fairly optimised Java bytecode whenever you give enough static type information (i.e. type hints or casts to primitive types). So in theory at least, you ought to be able to get pretty close to pure Java speed, which is itself pretty close to native code performance.

So let's prove it!

In this case, you have several issues that are causing the Clojure code to run slow:

  • Clojure supports arbitrary-precision arithmetic by default, so any arithmetic operations are automatically checked for overflow and if necessary numbers are promoted to BigIntegers etc. This extra checking adds a small amount of overhead which is usually negligible, but can show up if you run arithmetic operations in a tight loop like this. The easy way to fix this in Clojure 1.2 is to use the unchecked-* functions (this is a bit inelegant, but will be much improved in Clojure 1.3)
  • Unless you tell it otherwise, Clojure behaves dynamically and boxes function arguments. Hence I suspect your code is creating and boxing a lot of Integers/Longs. The way to remove this for your loop variables is to use primitive type hints and use constructs like loop/recur.
  • Likewise, the n is boxed, which means that the <= function call cannot be optimised to use primitive arithmetic. You can avoid this by casting n into a long primitive with a local let.
  • (time (some-function)) is also an unreliable way to benchmark in Clojure because it won't necessarily allow JIT compilation optimisations to kick in. You often need to run (some-function) a few times first so that the JIT has a chance to do its work.

My suggestion for the optimised Clojure version of add-up would therefore be something more like:

(defn add-up
  "Adds up numbers from 1 to n"
  [n]
  (let [n2 (long n)]                                    ; unbox loop limit
    (loop [i (long 1)                                   ; use "loop" for primitives
          acc (long 0)]                                 ; cast to primitive
      (if (<= i n2)                                     ; use unboxed loop limit
        (recur (unchecked-inc i) (unchecked-add acc i)) ; use unchecked maths
        acc))))

And a better way to time it is as follows (to allow JIT compilation to happen):

(defn f [] (add-up 10000000))
(do 
  (dotimes [i 10] (f)) 
  (time (f)))

If I do the above, I get 6 ms for the Clojure solution in Clojure 1.2. Which is something like 15-20x faster than the Node.js code and maybe 80-100x faster than your original Clojure version.

Incidentally, this is also about as fast as I can get this loop to go in pure Java so I doubt that it would be possible to improve this much in any JVM language. It also puts us at about 2 machine cycles per iteration... so it's probably not far from native machine code speed either!

(sorry not able to benchmark against Node.js on my machine, but it's a 3.3 GHz core i7 980X for anyone interested)


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon