I’m trying to run a simulation that has many different thermodynamic methods depending on the substance type (usually phase), and it’s nice to be able to easily choose what method to use. Long story short, I need to be able to call different thermodynamic function options at run-time based on a Substance type (basically a phase). They take the following form:
callfunc(T::DataType, x::ProcessStream)
The first argument that dispatches based on type (kind of like parse(Float64, str)).
I now built a fairly elaborate substance type system that works well with Julia’s dispatch system, but run-time dispatch can be slow, especially if I only need to consider root types or abstract phase classifications. Let’s say I want to write a “manual dispatch” function that runs on a list of stream types that changes once in a blue moon (making recompiling for every new list desirable). I’m considering the following options:
(1) Making a tuple type and dispatching on it. Here, I generate the list of types and create a Tuple with that type. I use that type as a dispatch argument, where the code runs on a for loop.
SUBSTANCE_TYPES = Tuple{root_types(Substance)...}
function stream_dispatch(callfunc::Function, x::ProcessStream, ::Type{typeList}=SUBSTANCE_TYPES) where typeList <: Tuple
for T in typeList.parameters
if x.subclass <: T
return callfunc(T, x)
end
end
#Retrun an error if x.subclass isn't in the SubstanceTypes
error("Process Stream $(x.id) contains invalid \"class\": $(x.subclass) \n>> Hint: class must be one of [$(join($typeList,", "))]")
end
I’m wondering, is the compiler is smart enough to know that “fieldtypes(typeList)” is a constant at JIT compile time? Does it know that the loop can be unrolled?
(2) Another option would be to make this function a generated function to build the big “if statement” with a call to root_types(Substance) internally at compile time. Obviously this generated function would need to be rebuilt if the Substance type hierarchy changes. One concern is that generated functions must be pure, and this generated function would need a potentially impure function passed as an argument. Is this good practice if the passed function observes a mutable state?
(3) Use ManualDispatch.jl. The only thing I might worry about for this option is that I’m not sure if it compiles for each different tuple value going in, because in this application, that value doesn’t change much, if at all.
I’m trying to strike a balance between performance and maintainability. I know that using generated functions (option 2) has some issues, but is potentially very fast. However, I’m wondering how performance would compare with the type-dispatch function (option 1) that’s pretty simple and comes with much less baggage. I also know ManualDispatch.jl was built to handle this sort of thing, but can it take advantage of the fact that the type set rarely changes?