Best practices for implicit function parameters

Often packages require passing callbacks with a fixed signature f(x) where my implementation of f will need to refer to an auxiliary variable in the surrounding context called a. Given how the use of global variables is generally discouraged in Julia, I set out to try a few different ways of achieving this and was surprised by the differences in their performance. I want share my findings with everyone and ask what you find a good trade-off between performance and interactivity (being able to redefine a in the REPL, for instance, is very useful).

In the code below we would like a function b(x) that does some computation based on the value of x as well as auxiliary data stored in a.

# auxiliary data
a = rand(10)

# a is dynamic
b1(x) = sum(x) + sum(a)

# a is static
b2 = let a = a
    x -> sum(x) + sum(a)
end

# a is static
makeb(a) = x -> sum(x) + sum(a)
b3 = makeb(a)

# a is static
module B
    const a = Main.a
    b(x) = sum(x) + sum(a)
end
b4 = B.b

# a is dynamic
b5(x) = sum(x) + sum(a::Array{Float64, 1})

# a is dynamic
b6(x, a) = sum(x) + sum(a)
b6(x) = b6(x, a)

# a is static
const c = a
b7(x) = sum(x) + sum(c)

# not ideal since you can't redefine this struct
# to add more context variables
struct MyContext
    a::Array{Float64, 1}
end

(c::MyContext)(x) = sum(x) + sum(c.a)
b8 = MyContext(a)

Timings for small arrays:

using BenchmarkTools

@benchmark b1($(rand(10)))
BenchmarkTools.Trial: 
  memory estimate:  48 bytes
  allocs estimate:  3
  --------------
  minimum time:     46.747 ns (0.00% GC)
  median time:      49.536 ns (0.00% GC)
  mean time:        51.960 ns (3.00% GC)
  maximum time:     1.668 μs (94.32% GC)
  --------------
  samples:          10000
  evals/sample:     988

@benchmark b2($(rand(10)))
BenchmarkTools.Trial: 
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     23.427 ns (0.00% GC)
  median time:      24.795 ns (0.00% GC)
  mean time:        26.231 ns (1.83% GC)
  maximum time:     1.659 μs (97.09% GC)
  --------------
  samples:          10000
  evals/sample:     996

@benchmark b3($(rand(10)))
BenchmarkTools.Trial: 
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     23.428 ns (0.00% GC)
  median time:      25.027 ns (0.00% GC)
  mean time:        26.318 ns (1.78% GC)
  maximum time:     1.616 μs (96.91% GC)
  --------------
  samples:          10000
  evals/sample:     996

@benchmark b4($(rand(10)))
BenchmarkTools.Trial: 
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     22.965 ns (0.00% GC)
  median time:      23.428 ns (0.00% GC)
  mean time:        24.691 ns (1.91% GC)
  maximum time:     1.623 μs (97.06% GC)
  --------------
  samples:          10000
  evals/sample:     996

@benchmark b5($(rand(10)))
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     9.206 ns (0.00% GC)
  median time:      9.327 ns (0.00% GC)
  mean time:        9.519 ns (0.00% GC)
  maximum time:     18.342 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     999

@benchmark b6($(rand(10)))
BenchmarkTools.Trial: 
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     23.930 ns (0.00% GC)
  median time:      24.393 ns (0.00% GC)
  mean time:        25.667 ns (1.84% GC)
  maximum time:     1.630 μs (97.02% GC)
  --------------
  samples:          10000
  evals/sample:     996

@benchmark b7($(rand(10)))
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     9.136 ns (0.00% GC)
  median time:      9.196 ns (0.00% GC)
  mean time:        9.195 ns (0.00% GC)
  maximum time:     14.722 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     999

@benchmark b8($(rand(10)))
BenchmarkTools.Trial: 
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     24.050 ns (0.00% GC)
  median time:      24.785 ns (0.00% GC)
  mean time:        26.049 ns (1.82% GC)
  maximum time:     1.636 μs (97.25% GC)
  --------------
  samples:          10000
  evals/sample:     996

Just use lexical scoping to capture the needed variables. e.g. if you have a function g(x,a), pass it as x -> g(x,a).

However, you need to benchmark this using functions, i.e. not in global scope. For example:

julia> X = rand(1000);

julia> f1(X) = sum(x -> x + 1, X);

julia> f2(X,a) = sum(x -> x + a, X);

julia> @btime f1($X);
  85.497 ns (0 allocations: 0 bytes)

julia> @btime f2($X, 1);
  88.206 ns (0 allocations: 0 bytes)
3 Likes