Creating an anonymous function at the local level. Conflict with const resulting in performance issues

I’m building a simulation framework and am running into the following problem:

I have a initialisation function init_model(x::InitType) where x is a structure containing the information needed for initialisation of a model. In the init function I define an anonymous function that uses some data from x. What data is used and how many fields are used is dependant on the specific InitType.

When I use a regular anonymous function I get a huge performance hit, which is apparently due to the fact that the anonymous function is not a const by default. However, I can not use the const keyword in a function. How would I solve this in an elegant way without loosing the flexibility of the simulation framework.

Here’s the (simplified) code:

struct Model
  sim_function::Function
end

function run_simulation(model::Model, steps::Int)
  for i in 1:steps
    model.sim_function(i)
  end
end

abstract type Init end

struct InitType <: Init
  increment::Int
end

function increment_sim(x::Int, increment::Int)
  return x + increment
end

function init_model(init::InitType)
  # This does not compile. This is the problem.
  # It compiles without the const keyword but then performance goes out the window.
  const sim_function = x -> increment_sim(x, init.increment) 

  return Model(sim_function)
end

init = InitType(2)

run_simulation(init_model(init), 3)

EDIT: I don’t really understand why the const keyword is not allowed in a local context since it is perfectly possible to define a named function in the local scope of another function and since all named functions are const by default I’m guessing this should also be possible for anonymous functions?

Two obvious issues before looking at this more closely:

This doesn’t compile. Provide an actual MWE.

Consult the Performance Tips page in Julia’s Manual. In particular, Function is not a concrete type, so this is likely to cause horrible performance.

I know the line with const does not compile. That is the problem. It compiles when I drop the const keyword but then my performance goes out the window.

The Function type does not cause (noticeable) performance issues if I use a named, and thus const, function.

The problem is the interaction between your loop and struct definition:

struct Model
  sim_function::Function
end

function run_simulation(model::Model, steps::Int)
  for i in 1:steps
    model.sim_function(i)
  end
end

The method run_simulation is only compiled once, since all its argument types are concrete. So the compiled code has no clue what sim_function is.

There are two standard approaches:

First, you can make Model parametric, a la struct Model{F<:Function} sim_function::F end. Now Model(identity) and Model((x)->x+1) will have different types, and run_simulation will be specialized to that type, permitting the compiler to statically dispatch and inline the thing.

This is sometimes inconvenient if you have code that needs to handle many different models with different sim_function.

The second approach is to add a function barrier, a la

function run_simulation(model::Model, steps::Int)
  foreach(model.sim_function, 1:steps)
end

You should read the implementation – it is a one-liner foreach(f, itr) = (for x in itr; f(x); end; nothing)

Now you will have a dynamic dispatch to specialize foreach on the function. This specialized function runs the loop and needs no more dynamic dispatches. Hence, you only pay one dynamic dispatch, instead of steps many.

^this, i.e. dynamic multiple dispatch with full specialization on argument types and resultant techniques like well-placed function barriers, is really the unique claim-to-fame of the julia language.

I especially want to emphasize the advantages over e.g. to C++ template contortions on the one side, and e.g. the graalVM/JIT specialization algorithm on the other side. (oh, the contortions to avoid profile cross-pollution in JVM!)

The mechanism in julia is both human legible and convenient (but it does force a certain coding style, and it’s a bad match for some problem domains where e.g. java excels).

2 Likes

Thanks!

I oversimplified the code though. The call to sim_function happens deeper in the call stack, not at the top level in the iteration loop. So it’s within a function that is called within the iteration loop of the simulation.

I guess I’ll need to drop the anonymous function solution and store the data in the model (in the full implementation I can store arbitrary data in the model). I just found the anonymous function solution more elegant.