Unexpected Allocations with Multiple Dispatch

Your General struct is (effectively) type-unstable because, although Julia knows that g.d is a DataType, it doesn’t know what datatype it is. It might be A, B, C, or D, or it might be Any or Int64 or Vector{Dict}. So when you call f4(g.d, g.n) Julia can’t statically resolve which method will be called or what it might return. It’s gotta pessimistically box up the return value and dynamically resolve its type at runtime with a pointer to arbitrarily-sized data. There’s your allocations.

So why doesn’t this happen with f3? Because Julia knows there are only a limited number (3) of methods… so it’s gotta be one of those. If it’s not, it’s an error, so it can just enumerate the possibilities from the method table itself. Instead of a boxed data with arbitrary types, it just puts in three branches, each of which is specialized with hard-coded dispatch to each particular method and specialized (on-stack/in-register) storage for the different return types. With a bit of (automatic) constant propagation you won’t even see those branches.

Add a fourth method to f3 — doesn’t matter what it is — and Julia will give up. It’ll invalidate all the functions it compiled that call f3 and ditch that optimization.

What’s the workaround? One option would be to ensure that the General struct knows — in its type! — what g.d is with a parameter:

struct General{T}
    d::Type{T}
    n::Int
end

Heck, now you don’t really even need that d member at all — the information is in the type parameter itself.

struct General{T}
    n::Int
end
General(::Type{T}, n) where {T} = General{T}(n)
5 Likes