The Problems
The point others are making here is that this is an apples-and-oranges comparison. Your fun_foreach
and fun_eval
functions are operating on raw expressions, while the others have the benefit of already processing your expressions before they are called.
In your case, fun_func
and fun_file
are literally identical and any benchmarking differences are coincidental. The only difference being that fun_file
went on a roundabout trip through your harddrive as a file first (but that part was excluded from benchmarking). If you had actually timed the process of writing and include
ing the fun_file
version, it would be extremely slow by comparison.
You have a typo in fun_func
where you don’t assign to a[3]
in the first line.
You have a typo where you pass evec
rather than eblk
to fun_eval
.
eval
does not work how you think. Any expression that is eval
’d is evaluated in GLOBAL scope of the current module. That means that your attempt to pass a
to fun_foreach
and fun_eval
is fruitless. The fact that they work at all is merely a coincidence resulting from a
being a global variable (and thus accessible in the global scope where the eval
occurs).
For the scoping reasons I just discussed, I am virtually certain the @inbounds
annotations in fun_foreach
and fun_eval
are not doing anything. But a few missing inbounds annotations are not really the biggest issues here.
Solutions
As suggested by others: if you are writing functions to call them once, you should forego any hopes of “fast” run times in Julia. If you’ll be calling them many times, you can mostly get away with this. However, I would encourage you to figure out whether you can avoid building expressions (or worse, strings) to call in eval
altogether.
Your genfun
approach would be my preference (well, fun_func
is actually but you seem to be indicating that isn’t viable since you’re generating expressions). Part of the poor performance of fun_blk
is a benchmarking artifact of the fact that fun_blk
is a variable (that is bound to an anonymous function) rather than a function like all the others. As such, you need to interpolate it into the benchmarking expression.
I’ve taken the liberty of updating your tests, although I dropped fun_foreach
and fun_eval
because of the not-actually-using-a
issue.
using BenchmarkTools
a = [1,1,0,0]
evec = [:(@inbounds a[3] = a[1] + a[2]), :(@inbounds a[4] = a[2] + a[3]), :a]
# as function
function fun_func(a)
@inbounds a[3] = a[1] + a[2]
@inbounds a[4] = a[2] + a[3]
a
end
function genfun(expr, args::Tuple, gs=gensym())
eval(Expr(:function,Expr(:call, gs, args...),expr))
(args...) -> Base.invokelatest(eval(gs), args...)
end
function genfun_alt(expr, args::Tuple, gs=gensym())
fun = eval(Expr(:function,Expr(:call, gs, args...),expr))
(args...) -> Base.invokelatest(fun, args...)
end
function genfun_noinvoke(expr, args::Tuple, gs=gensym())
fun = eval(Expr(:function,Expr(:call, gs, args...),expr))
end
eblk = Expr(:block,evec...)
fun_blk = genfun(eblk, (:a,))
fun_blk_alt = genfun_alt(eblk, (:a,))
fun_blk_noinvoke = genfun_noinvoke(eblk, (:a,))
# fun_func is a function (global const) so interpolation is unnecessary
# fun_blk and friends are global variables so need to be interpolated
@btime fun_func($a) # 6.000 ns (0 allocations: 0 bytes)
@btime $fun_blk($a) # 149.095 ns (0 allocations: 0 bytes)
@btime fun_blk($a) # 171.982 ns (0 allocations: 0 bytes) <- not interpolated
@btime $fun_blk_alt($a) # 34.038 ns (0 allocations: 0 bytes)
@btime $fun_blk_noinvoke($a) # 6.400 ns (0 allocations: 0 bytes)
Notice that I added the @inbounds
annotations directly to the relevant expressions. I defined an alternative genfun_alt
that avoids an excess eval
and a genfun_noinvoke
that does not use invokelatest
. There are technical reasons invokelatest
can be necessary, but note that skipping it saves 150ns of overhead. I’m not really qualified to explain when invokelatest
might and might not be required, but it has something to do with world age. Unfortunately, I vaguely suspect that your stated use case (rather than this demo) might be one of those cases (the same issue would also thwart your fun_file
).