Is NodeJs faster than Clojure?

17,162

Solution 1

(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

Solution 2

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)

Solution 3

A high-level comment. Node.js and Clojure have completely different models for attaining scalability and eventually making software run fast.

Clojure achieves scalability through multi-core parallelism. If you build your Clojure programs correctly, you can divvy up your computational work (via pmap, etc.) to ultimately run in parallel on separate cores.

Node.js is not parallel. Rather its key insight is that scalability (usually in a web application environment) is I/O bound. So Node.js and Google V8 technology attain scalability via many asynchronous I/O callbacks.

Theoretically, I would expect Clojure to beat Node.js in areas easily parallelizable. Fibonacci would fall in this category and would beat Node.js if given enough cores. And Node.js would be better for server-side applications that make many requests to the file system or network.

In conclusion, I do not think this may be a very good benchmark for comparing Clojure versus Node.js.

Solution 4

Couple of hints, assuming you're using clojure 1.2

  • repeating the (time ...) tests is likely to gain higher speeds in clojure, because of JIT optimizations kicking in.
  • (inc i) is - a little - faster than (+ i 1)
  • the unchecked-* functions are also faster (sometimes MUCH faster) than their checked variants. Assuming you don't need to exceed the limit of longs or doubles, using unchecked-add, unchecked-int etc could be a lot faster.
  • read up on type declarations; in some cases, they can also improve speed substantially.

Clojure 1.3 is generally faster with numerics than 1.2, but it's still under development.

The following is about 20 times faster than your version, and it can still be improved by modifying the algorithm (counting down, like the js version does, instead of up saves a binding).

(defn add-up-faster
  "Adds up numbers from 1 to n"
  ([n] (add-up-faster n 0 0))
  ([^long n ^long i ^long sum] 
    (if (< n i)
      sum
      (recur n (unchecked-inc i) (unchecked-add i sum)))))

Solution 5

Not directly related to optimization problem at hand but your Fib can be easily sped up:

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

change to:

(def fib (memoize (fn
  [n]
  (if (<= n 1) 1
      (+ (fib (- n 1)) (fib (- n 2)))))))

Works much faster (from 13000 ms for fib 38 on core i5 - why is my computer slower than dualcores? - to 0.2 ms). In essence it isn't much different than an iterative solution - though it does allow you to express the problem in a recursive fashion for the price of some memory.

Share:
17,162
foobar
Author by

foobar

Updated on June 27, 2022

Comments

  • foobar
    foobar almost 2 years

    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.