MiniLight is a minimal global illumination renderer that has been translated to 7 languages and takes on average 570 lines of code. Neat! It's of course accompanied by some benchmarks. The results are unsurprising: C++ is the largest (951 lines) and fastest, followed by OCaml (461 LoC, 2.7X slower on x86 --- this is relevant because it's faster on AMD64) and Scala (427 LoC, 6.7X slower). The slowest versions are unsurprisingly Ruby (498 LoC, 575X slower) and Python (490 LoC, 173X slower); these languages have a hard time here, not being well-suited for numerical nor symbolic processing (both in terms of implementation effort and efficiency).
I took a look at the OCaml code and couldn't resist the temptation to clean it up a bit (I did not optimize it thoroughly, though, so there's still ample optimization potential --- the same can be said of all implementations, I suspect). You can find the modified MiniLight code here.
I essentially massaged the parts that screamed for a cleanup (some bad practices explained below), making the code 17 lines shorter in the process and the program substantially faster. I then realized that whereas the C++ version was using a very fast custom PRNG, the OCaml translation utilized the stdlib one, with an appreciable effect on the speed:
Version
LoCs
smits-large processing time
C++
951
15.6
OCaml orig
461
24.5
OCaml, idiomatic pattern matching
448
15.8
OCaml, simpler record type for triangle
444
14.7
OCaml, custom PRNG
452
11.6
(AMD64, GCC 4.3.2, OCaml 3.11.1 with -nodynlink)
By using more idiomatic pattern matching and option types, the OCaml version goes from being 57% slower to just as fast as the C++ one, with fewer lines of code and more static safety (see below). With the same PRNG as the C++, the OCaml version is now 34% faster than the C++. Both admit doubtlessly further optimizations. Algorithmic ones will be easier to express in OCaml, but C++ will have the edge in terms of low-level code optimization.
Anti-pattern: if instead of pattern matching
Most of the speed increase was brought by moving from ugly if expressions to idiomatic pattern matching. The raytracer had code like
let emissionIn =
if (Triangle.Null = hitObject) || ((Triangle.Obj emitter) = hitObject) then
...
else
vZero
in
This feels wrong: when you have a variant type, you definitely want to pattern match, not to use if. As it turns out, the above code is also slower than pattern matching for two reasons:
Triangle.Obj emitter allocates a block in the heap that will be dead right away
it uses structural comparison (meaning that the object/record fields are compared one by one) instead of identity comparison
This is a simpler, more idiomatic way to write the above, which is also much faster (I use the standard option type instead of a new one that serves no purpose):
let emissionIn = match hitObject with
Some emitter' when emitter' != emitter -> vZero
| _ -> ...
It's easier to read too, as you don't have to analyze two boolean expressions, but only one.
Anti-pattern: tuples with boolean flags instead of option types