Closures and type inference

Hi there,

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.

Am I missing something?

Thanks a lot!

1 Like

Is that the actual example provided in the learning material? There’s no performance problem with this example.

Is it possible your learning material was written by an LLM chatbot? They often get things like this wrong.

1 Like

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}

So the type is known in this case.

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:

2 Likes

Thanks, this is useful!

1 Like

For the record, this is an experimental rewrite of Julia’s lowering compiler pass:

AFAIK one of the design goals is allowing closures to be handled more efficiently.

1 Like

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.