There exists a tension between readability and speed. This is discussed briefly in Keeping track of simple vs high-performance versions - Usage / Performance - JuliaLang.
Indeed, I often write two versions of a function,
- V1: Naive implementation. Since Julia is so expressive, this implementation is usually short and resembles the published equations or pseudocode.
- V2: Optimized implementation. This version is written for a computer, i.e. \subset \{ exploits symmetries, reuses allocated memory, hits the cache in a friendly way, reorders calculations for SIMD, divides the work with threads, precomputes parts, caches intermediate expressions, \dots \}.
V1 is easier to understand and extend. V2 is the implementation exported in your package and it’s often much faster, but complicated and verbose. Julia sometimes allows you to use abstractions such that V1 \approx V2, but this is not always possible.
I’ve thrown together Slow.jl, which lets you keep both versions by exporting macros @slowdef
and @slow
. We define the function with @slowdef
, which injects a first argument of the type Slow.SlowImplementation
for dispatch. This slow implementation is callable with @slow(f(args...))
, which is sugar for f(Slow.SlowImplementation(), args..)
. It’s basically a shortcut for a multimethod with two options. If you have three different implementations, you probably should just define your own algorithm type for the usual multimethod pattern.
Example
using Slow
# V1: replaces this signature with f(::Slow.SlowImplementation, x, y)
@slowdef function f(x, y)
sleep(1)
return sin(x)
end
# V2: our exported fast implementation
function f(x, y)
# blah blah complicated optimizations
return sin(x)
end
@time @slow f(1.0, 1.0) # calls f(Slow.SlowImplementation(), 1.0, 1.0)
@time f(1.0, 1.0)
1.001939 seconds (1.80 k allocations: 101.328 KiB)
0.000000 seconds
I’d love to hear about alternative approaches!