icfpc2019-rust icon indicating copy to clipboard operation
icfpc2019-rust copied to clipboard

Re-implementaion of https://github.com/tonsky/icfpc2019 in Rust to compare performance

Re-implementaion of github.com/tonsky/icfpc2019 in Rust to compare performance.

Problem

icfpcontest2019.github.io

Building and running

RUST_BACKTRACE=1 cargo run problems/prob-049.desc --interactive

Running release version:

cargo run --release problems/prob-049.desc

Solve all:

cargo run --release problems/*.desc --threads=12

Performance comparison

Code versions:

Test machine: MacBook Pro (15-inch, 2018), 2.6 GHz Core i7, 6 cores w/ Hyper-Threading, 16 GB 2400 MHz DDR4, macOS 10.14.5.

Baseline algorithm, utilizing twelve threads (contest):

Solution Time, min Time, ms Relative speed
Clojure w/ JDK 12.0.1+12 15.4 min 926698 ms x1 (baseline)
Clojure w/ GraalVM EE 19.1.0 10.6 min 638876 ms x1.45
Clojure w/ Native Image 18.4 min 1105028 ms x0.83
Rust baseline 0.87 min 52460 ms x17.8
Rust + LTO 0.85 min 51154 ms x18
Rust + FNV hash 0.48 min 28964 ms x32

Utilizing single thread:

Solution Time, min Time, ms Relative speed
Rust baseline 4.2 min 251625 ms x3.7
Rust + FNV hash 2 min 124356 ms x7.45

Further improvements:

Solution Time, min Time, ms Relative speed
Rust + blockers vector 0.41 min 24892 ms x37.2
JoinR Clojure w/ JDK 12.0.1+12 1.9 min 114582 ms x8.1
JoinR Clojure w/ GraalVM CE 19.1.1 1.6 min 93582 ms x9.9

(time: lower is better, speed: bigger is better)

Some conclusions

Disclaimer:

The point of this experiment was to show how average code in different languages perform. Not some perfect, pushed to the limit code that some genius spent infinite time polishing. No, regular code that we write every day under time and resource pressure.

I also don’t try to slander Clojure specifically. I didn’t try to write slow and inefficient Clojure code on purpose. I didn’t try to make Rust code to cheat. This is a fair and honest comparison of how things are if you are just using them, day to day.

Clojure is still a great language for quick hacking and interactive exploration. I was just curious how much am I missing performance-wise.

That said,

  • Naive Rust wins over naive Clojure ~20x while doing the exact same thing.
  • Rust on a single thread performs better than Clojure on 12 threads.
  • GraalVM gives your JVM program 1.5x boost basically for free.
  • Clojure can be pushed really hard to be just ~4x slower than Rust version written by a complete beginner.

My personal highlights from seeing JoinR and Serioga findings:

  • Clojure defrecords are terribly slow at computing hashes. They treat it like a generic map even though list of all fields is known to defrecord macro in advance.
  • (< 1 x 2) is still slow after three years of reporting it
  • Destructuring is slow :(
  • Most of clojure.core methods like get/nth are polymorphic hence slow. .valAt on maps is much faster if you know it’s a map. You usually do.
  • Getting rid of boxing warnings is almost impossible in a big codebase. At least, if you plan to use functions. Or datatypes more compact than long and double.

At this point you’ll be writing Java code in Clojure syntax anyway. So just swith to Java and don’t try making Clojure performant by making it look and work like Java. There’s no point in writing something that is only nominally Clojure.