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 includeing 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).