Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is this Clojure program so slow? How to make it run fast?

Here it is clearly explained how to optimize a Clojure program dealing with primitive values: use type annotations and unchecked math, and it will run fast:

(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)))))

So, just out of curiosity, I've tried it in lein repl and, to my surprise, found this code running ~20 times slower that expected (Clojure 1.6.0 on Oracle JDK 1.8.0_11 x64):

user=> (time (add-up 1e8))
"Elapsed time: 2719.188432 msecs"
5000000050000000

Equivalent code in Scala 2.10.4 (same JVM) runs in ~90ms:

def addup(n: Long) = { 
  @annotation.tailrec def sum(s: Long, i: Long): Long = 
    if (i == 0) s else sum(s + i, i - 1)
  sum(0, n)
}

So, what am I missing in the Clojure code sample? Why is it so slow (should theoretically be roughly the same speed)?

like image 831
Ivan Mikushin Avatar asked Jul 28 '14 22:07

Ivan Mikushin


People also ask

Why is Clojure so slow?

Clojure projects are slow to start not only because of Clojure — Clojure itself starts in ~1 second — but because of Clojure specifics, the namespaces, especially not AOT-compiled one, are loaded slowly.

Is Clojure fast?

In principle, Clojure can be just as fast as Java: both are compiled to Java bytecode instructions, which are executed by a Java Virtual Machine ... Clojure code will generally run slower than equiva- lent Java code. However, with some minor adjustments, Clojure performance can usually be brought near Java performance.

Why is lein repl so slow?

If you run lein repl from within a project directory, it will load your project's source files in addition to starting a repl. Even for a small project, this can add significant time if your source files reference external dependencies.

Is clojure efficient?

Idiomatic Clojure without sacrificing performance This made me curious as to how fast it could be made if I really tried, and was able to get nearly 30x more1 by optimizing it. Clojure is definitely fast enough for everything I've done professionally for six years.


2 Answers

Benchmarking with lein repl is generally a bad idea as it specifically sets non-server JVM settings. Using the Clojure JAR directly I see ~40ms on a 3.5ghz i7 iMac running JDK 8 under OS X 10.9.

like image 109
dnolen Avatar answered Oct 09 '22 13:10

dnolen


Further to @dnolen's answer, a few observations:

Though it turns out to make no real difference, we should make the Clojure function the same shape as the Scala one. In

(defn add-up ^long [^long n]
  (loop [n n i 0 sum 0]
    (if (< n i)
      sum
      (recur n (inc i) (+ i sum)))))
  • n is not changed by the recur, so need not be bound in the loop.
  • The two remaining arguments are in the opposite order to the Scala function.
  • It runs up the numbers, whereas the Scala runs down.

Mending these inconsistencies, we get

defn add-up [^long n]
  (loop [sum 0, i n]
    (if (zero? i)
      sum
      (recur (+ sum i) (dec i)))))

(The Scala type system ensures that the argument n is converted to a Long on call. As I understand it (please correct me if I'm wrong), the Clojure ^long type hint promises to treat a Long argument well, but does not promise to convert a Double like 1e8 to a Long. But I got very inconsistent results when I made the corresponding changes.)

On my laptop, the above gives

(time (add-up 100000000))
"Elapsed time: 103.636782 msecs"
5000000050000000

If you remove the type hint

(defn add-up [n]
  ...
  )

... the elapsed time multiplies by about twenty:

(time (add-up 100000000))
"Elapsed time: 2374.399915 msecs"
5000000050000000
  • This has more effect than removing unchecked math, which roughly triples the elapsed time.
  • Type hinting the return type has no discernible effect.

All this on Clojure 1.5.0 on OpenJDK Java 7.

like image 5
Thumbnail Avatar answered Oct 09 '22 15:10

Thumbnail