Jan Rychter: blog (electronics, programming, technology)

Clojure performance revisited

2009-07-29

Since many people asked me about this, here are some additional notes about Clojure performance.

First, something which came to me as a surprise: the single biggest performance jump I got with my application was achieved by switching from Java 5 to Java 6 (64-bit, Mac OS X). The jump was huge — from interpreting around 850,000 instructions per second right up to 1,300,000 instr/s. That’s a nearly 60% improvement that required ZERO work on my part. Two clicks in Java Preferences.

Invoking a function is expensive. I am back to old Common Lisp techniques of using macros instead of functions in many places.

Watch out for var lookups (yes, I mentioned this before, but this is important).

The other things I did were application-specific, so there isn’t much point in describing them here.

And if you’re interested in how the JIT performs, here’s a sample run of the application. As you can see, it takes almost 20 runs until the times stabilize at around 1.5 million interpreted instructions per second. The improvement is dramatic: 276% from the first run to the last one.


Executed 616154 instructions in 1.543867 seconds, instruction rate: 399097.84 inst/s
Executed 616154 instructions in 0.653465 seconds, instruction rate: 942902.9 inst/s
Executed 616154 instructions in 0.522443 seconds, instruction rate: 1179370.8 inst/s
Executed 616154 instructions in 0.492671 seconds, instruction rate: 1250639.9 inst/s
Executed 616154 instructions in 0.482119 seconds, instruction rate: 1278012.2 inst/s
Executed 616154 instructions in 0.424934 seconds, instruction rate: 1449999.2 inst/s
Executed 616154 instructions in 0.424169 seconds, instruction rate: 1452614.4 inst/s
Executed 616154 instructions in 0.416273 seconds, instruction rate: 1480168.1 inst/s
Executed 616154 instructions in 0.420429 seconds, instruction rate: 1465536.4 inst/s
Executed 616154 instructions in 0.421797 seconds, instruction rate: 1460783.2 inst/s
Executed 616154 instructions in 0.421114 seconds, instruction rate: 1463152.5 inst/s
Executed 616154 instructions in 0.4115 seconds, instruction rate: 1497336.5 inst/s
Executed 616154 instructions in 0.410837 seconds, instruction rate: 1499753.0 inst/s
Executed 616154 instructions in 0.411064 seconds, instruction rate: 1498924.8 inst/s
Executed 616154 instructions in 0.410936 seconds, instruction rate: 1499391.6 inst/s
Executed 616154 instructions in 0.410301 seconds, instruction rate: 1501712.1 inst/s
Executed 616154 instructions in 0.410638 seconds, instruction rate: 1500479.8 inst/s
Executed 616154 instructions in 0.408832 seconds, instruction rate: 1507108.0 inst/s
Executed 616154 instructions in 0.410466 seconds, instruction rate: 1501108.5 inst/s
Executed 616154 instructions in 0.410113 seconds, instruction rate: 1502400.5 inst/s
Executed 616154 instructions in 0.409741 seconds, instruction rate: 1503764.5 inst/s

Comments

I think the roots of slowness of calling functions and accessing vars are the same, because defn is tantamount to def'ing a var with a functional value. But Clojure does have inline functions (see core.clj, though I think it's not capable of automatically inlining things it deems necessary.

Daniel Janus2009-07-29

Yes, part of the cost of invoking a function is a var lookup -- but not only. My interpreter stores functions in local maps, and even if you omit the lookup, the actual invocation is still the #1 item in a profiler hot spot list.

Jan Rychter2009-07-30