I am reading through some Julia learning material and I had a question that someone here might be able to help with.
The example given reads:
function add_n_fn(n)
function _add_n(x)
return x+n
end
end
add_5 = add_n_fn(5)
add_5(10) == 15
and the text reads: “this approach can cause huge performance issues, as the types of the captured variables cannot always be known”
I don’t fully understand this. How can the type not be known as runtime, since it is explicitly entered by the user? I think that what it could happen here that would lead to type instability is that the compiler can not guarantee that the type of the captured variable will remain the same by the time it needs to operate on it. However, I believe this is a different statement than: “can not always be known” at runtime.
Yes thats the actual example in the material I am reading (This is not official Julia learning material). It was not written by LLM, its from notes on HPC in Julia from a UK university (I think its best not to disclose the actual source).
So is your take that this closure can not cause type instability issues? Is my intuition about the compiler not being able to guarantee that the variable will be used in operation with the same type as runtime type not an issue for type instability?
Notice that the type of the closure is actually parameterized by the type (Int64 in this example) of the captured variable:
julia> function add_n_fn(n)
function _add_n(x)
return x+n
end
end
add_n_fn (generic function with 1 method)
julia> add_n_fn(3)
(::var"#_add_n#add_n_fn##0"{Int64}) (generic function with 1 method)
julia> typeof(ans)
var"#_add_n#add_n_fn##0"{Int64}
Yep, this is my understanding too but in trying to understand why this function was used as an example of type instability, my question is:
is it not problematic that I can change its type inside the function and therefore the compiler can not specialize to Int the sum function in the return?
Mutating captured variables is problematic. Whether the compiler will be able to generate efficient code will depend on the exact code and compiler version. Have you checked out the relevant section in the Performance tips in the Julia Manual: Performance of captured variable?
Also, this is the main issue on Julia’s bug tracker on Github:
They might be erring on the side of caution for beginners. Personally I think “optimaly, don’t reassign captured variables” would be simple enough, but beginners might forget that tidbit or forget that a particular variable is captured and shouldn’t be reassigned in edits, and Julia will happily compile unoptimal code to reassign a captured variable for us to facepalm after profiling our wider program. I bet the material encourages explicit callable objects with annotated fields.
You are right, that is what it encourages: Functors, structs which inherit from the Function type. However, I find the tips given in the link given by @nsajko more intuitive.
Ooh that could be another performance gotcha. Subtypes of Function are not specialized as arguments of a method if the method doesn’t call them directly, this is a heuristic against excessive compilation you must opt out of with method parameters (described in the Performance Tips as well) or @inlineing. Thing is, “functor” types are just normal types, and those don’t subtype Function unless you explicitly write so; there is a gotcha in the other direction when heavy use of callable objects through long call chains results in overcompilation that aren’t caused by Function subtypes.