Allocations when passing closure as parameter

I struggling to obtain a minimal working example. But I have one situation which looks like:

function outer!(f::Function, output,x::AbstractVector) 
  for i in 1:10
    output = inner!(f,output,x)
  end
  return output
end

f is given by the user, most likely as a closure. Something like:

function run()
  a = 5
  f(x,a) = a*x
  output = outer!( x -> f(x,a), output, x)
end

I am seeing some allocations associated with this closure. Curiously, if I modify the outer! function to:

function outer!(f::Function, output,x::AbstractVector) 
  g = (x) -> f(x)   # <<< set g here as pass g in the loop to inner!
  for i in 1:10
    output = inner!(g,output,x) 
  end
  return output
end

or to

function outer!(f::Function, output,x::AbstractVector) 
  for i in 1:10
    output = inner!((x) -> f(x),output,x)   # pass a closure instead of `f` directly
  end
  return output
end

the allocations go away. It seems that telling the compiler how many parameters f is what is solving the problem.

Maybe this is related to this known issue. But I am not completely sure.

As a workaround, adding g = (x) -> f(x) in my case is not a problem, but it seems like a hack to something I am not quite understanding.

Any insight is appreciated.

By the way: I cannot find type instabilities anywhere. Does not seem to be related to that. Also there is not such a big performance penalty (as reported in the issue). Just some some thousand allocations more.

https://docs.julialang.org/en/v1/manual/performance-tips/#Be-aware-of-when-Julia-avoids-specializing

As a heuristic, Julia avoids automatically specializing on argument type parameters in three specific cases: Type , Function , and Vararg . Julia will always specialize when the argument is used within the method, but not if the argument is just passed through to another function.

So your non-allocating example uses f, and thus the method is (presumably) specialized for f, while the allocating examples just pass it through, so specialization is avoided.

Be aware that adding the where annotation for f will lead to compilation for each new f.

1 Like

Great, that was exactly what was going on. (I probably want it to specialize for every f in this case, although I am not sure if that will have any impact on performance - getting rid of those unexpected allocations is nice anyway).