Function inside struct allocates when referenced

FunctionWrappers is not this blind, it safeguards the types. The call signature restriction parametizes the wrapper with the intended return type and argument types, and the wrapper constructor takes a function as an argument.

  1. Taking the function as an argument is almost self-explanatory, we are trying to group function calls together in a language where functions don’t have fixed input types and return types. So, a concrete wrapper type constructor must take any function as an input.

  2. Fixing the return type provides type stability of the functions’ output, which is the bare minimum of categorizing functions in statically typed languages. Since there’s no guarantee of type stability of 1 method, let alone several functions, a convert step of the return value is added after calling the wrapped function. This accommodates type-unstable methods, but a convert with an uninferred input would need a runtime dispatch so it’s best to stick to type-stable methods if you’re going this far to avoid runtime dispatches and allocations.

  3. Fixing the argument types provides the rest of the necessary information to dispatch and compile a function call given the wrapped function, and the wrapper stores the compiled code to skip over the typical dispatch mechanism. A convert step of the input arguments is also added before calling the wrapped function. (Hypothetically, function calls could be grouped by return type alone so the argument types could be a field instead of parameters of the wrapper type, but it seems a little crazy to micromanage that for every element of an array of call signatures only to probably input the same argument types every iteration.)

So a FunctionWrapper incurs a runtime cost to make sure what goes in and out of the wrapped compiled code is type-safe, though several converts are usually cheaper than a runtime dispatch. EDIT: those converts might be no-ops if the compiler infers the inputs already are the target type. Also the stored compiled code is not updated when the original method is edited, so use reinit_wrapper on existing instances or replace them entirely.

I’ve never used it, but there is a package FunctionWranglers.jl that doesn’t incur this cost. Rather than wrapping a function one by one, it takes in entire arrays of functions and uses recursively inlined generated functions to unroll loops over them in a set of higher order functions. Think the if-statements checking the function in Variant 4, but the if-statements check the index so the same function at different indices still get their own branch. I’ve never reached for this because the map-like function requires the same input values for each call, indexing the wrangled array isn’t constant time (again, massive unrolled loop), and you can forget about any array mutations after it gets wrangled into recursive immutable struct instances. The repository hasn’t been touched for a couple years but I’m guessing it works because the package has no dependencies and I haven’t spotted any internal or unstable features in the short source file.

2 Likes