I am creating some numerical methods, and I am running into some issues with closures.
Here’s an example of what I’m doing:
Function to drive myPkg:
function main()
#allocate temporaries
function linear()
# Use temps and do stuff
end
function nonlinear()
# Use temps and do stuff
end
myPkg.solver(retVal, linear, nonlinear, otherParams)
end
Function inside my module:
function solver(retval, linear, nonlinear, otherParams)
for i = 1:N
linear(retVal, otherParams)
nonlinear(retVal, otherParams)
end
return retVal
end
My problem is that an excessive amount of allocations take place and the function is over ten times slower than if I encapsulate the functions inside of the module. I’m not certain if my concept of a closure is completely wrong, or if the way I have it causes additional temporaries to be created that I don’t see.
If you want a more concrete example checkout my GitHub for comparison of the module that encapsulates the functions (https://github.com/cpross90/BenjaminOnoSolver.jl) and the module that accepts external functions (https://github.com/cpross90/SplitStep.jl). I’m looking to have the module work similarly to how the Optim.jl package receives objective functions for a cost function/gradient/hessian.
If there are any suggestions to how I can tackle this better I am all ears. Just an undergraduate applied math researcher trying to get a hold of Julia.
Here’s a concrete example of what @stevengj is describing:
julia> function solver(f)
minimum(f, 1:10)
end
solver (generic function with 1 method)
julia> function slow()
# allocate temporaries
x = 1
# define inner function
function inner(y)
x = y
y
end
solver(inner)
end
slow (generic function with 1 method)
The telltale sign of issue 15276 is the Core.Box in @code_warntype:
As a general coding pattern to teach to people, do you always avoid this issue with 100% certainty if you have a closure over a struct, Tuple or a NamedTuple?
A let block is necessary when you see a Core.Box around a variable which is read but whose binding does not change. If you need to rebind a variable, then you can instead use a Ref and mutate that Ref. My general experience has been that the first issue (Boxing of variables which are not re-bound) is less common now, but the second issue (Boxing of variables whose bindings need to change) is still likely.
Yes. My issue is trying to come up with coding patterns for beginners to avoid this. They have enough trouble understanding basic scoping, and won’t be able to interpret warntype.
OK, that is helpful. When you say “less common”, any sense of the frequency? Assuming that you have a set of users who will never be able to reliably run @code_warntype, do you think they can safely avoid this in most cases these days if they don’t rebind? I think I can teach people not to rebind, as it is easy to visually parse the x = ...
Wow, this is awesome, thanks to everyone who’s left something here.
It appears that I might be having other type stability issues elsewhere. I’m not seeing anything in @code_warntype that would indicate type instability in the closure, so I’m going to comb through the module more carefully. This definitely helps give me a better idea of how to implement closures. Thanks again!