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)?
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.
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.
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.
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.
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.
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
.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
All this on Clojure 1.5.0 on OpenJDK Java 7.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With