Benchmarking ADTs behavior

I am trying to get the most performant way to retrieve some properties associated to an ADT (I am using Moshi.jl, but I suspect the behavior with LightSumTypes et al. would be similar). Below I paste the code I am testing and the speeds I am benchmarking.

How is it possible to get mor performance out of ImmutableDict + manual dispatch than manual dispatch only?

Code definitions
import Base: ImmutableDict

using Moshi.Data: @data
using Moshi.Match: @match

abstract type AbstractADT end

@data MyADT <: AbstractADT begin
    A
    B
    C
    D
    E
    F
    G
    H
    I
    J
    K
    L
    M
    N
end

import .MyADT
const ADTType = MyADT.Type

struct ADTProperties
    p1::Int64
    p2::Float64
    p3::String
    p4::Char
end

const D_ADT::ImmutableDict{ADTType, ADTProperties} = ImmutableDict(
    MyADT.A() => ADTProperties(1,1.0,"1",'1'),
    MyADT.B() => ADTProperties(2,2.0,"2",'2'),
    MyADT.C() => ADTProperties(3,3.0,"3",'3'),
    MyADT.D() => ADTProperties(4,4.0,"4",'4'),
    MyADT.E() => ADTProperties(5,5.0,"5",'5'),
    MyADT.F() => ADTProperties(6,6.0,"6",'6'),
    MyADT.G() => ADTProperties(7,7.0,"7",'7'),
    MyADT.H() => ADTProperties(8,8.0,"8",'8'),
    MyADT.I() => ADTProperties(9,9.0,"9",'9'),
    MyADT.J() => ADTProperties(10,10.0,"10",'A'),
    MyADT.K() => ADTProperties(11,11.0,"11",'B'),
    MyADT.L() => ADTProperties(12,12.0,"12",'C'),
    MyADT.M() => ADTProperties(13,13.0,"13",'D'),
    MyADT.N() => ADTProperties(14,14.0,"14",'E'),
)

function get_property_manual(adt::ADTType)
    x = @match adt begin
        MyADT.A() => D_ADT[MyADT.A()]
        MyADT.B() => D_ADT[MyADT.B()]
        MyADT.C() => D_ADT[MyADT.C()]
        MyADT.D() => D_ADT[MyADT.D()]
        MyADT.E() => D_ADT[MyADT.E()]
        MyADT.F() => D_ADT[MyADT.F()]
        MyADT.G() => D_ADT[MyADT.G()]
        MyADT.H() => D_ADT[MyADT.H()]
        MyADT.I() => D_ADT[MyADT.I()]
        MyADT.J() => D_ADT[MyADT.J()]
        MyADT.K() => D_ADT[MyADT.K()]
        MyADT.L() => D_ADT[MyADT.L()]
        MyADT.M() => D_ADT[MyADT.M()]
        MyADT.N() => D_ADT[MyADT.N()]
    end::ADTProperties
    return x.p1
end

function get_property_auto(adt::ADTType)
    x = D_ADT[adt]
    return x.p1
end

function get_property_full_manual(adt::ADTType)
    return @match adt begin
        MyADT.A() => ADTProperties(1,1.0,"1",'1')
        MyADT.B() => ADTProperties(2,2.0,"2",'2')
        MyADT.C() => ADTProperties(3,3.0,"3",'3')
        MyADT.D() => ADTProperties(4,4.0,"4",'4')
        MyADT.E() => ADTProperties(5,5.0,"5",'5')
        MyADT.F() => ADTProperties(6,6.0,"6",'6')
        MyADT.G() => ADTProperties(7,7.0,"7",'7')
        MyADT.H() => ADTProperties(8,8.0,"8",'8')
        MyADT.I() => ADTProperties(9,9.0,"9",'9')
        MyADT.J() => ADTProperties(10,10.0,"10",'A')
        MyADT.K() => ADTProperties(11,11.0,"11",'B')
        MyADT.L() => ADTProperties(12,12.0,"12",'C')
        MyADT.M() => ADTProperties(13,13.0,"13",'D')
        MyADT.N() => ADTProperties(14,14.0,"14",'E')
    end
end
Benchmarks
julia> @b rand(keys(D_ADT)) get_property_manual
2.057 ns

julia> @b rand(keys(D_ADT)) get_property_full_manual
2.280 ns

julia> @b rand(keys(D_ADT)) get_property_auto
2.285 ns

I think that the discrepancy is due to the fact that creating ADTProperties has some overhead so get_property_full_manual is in disadvantage. I expect that the perf difference disappears/be better if you instead store ADTProperties inside the ADT types.

Thanks for the answer!

I don’t get what you say about get_property_full_manual: it does not create an ADT at all.

I thought about storing the properties inside, but I have two problems.

First, if I am not wrong, it would make them heavier to handle down to functions, since they are full isbits and they would be copied around? Or maybe this is not at all like C, since they are immutable and it doesn’t matter.

Second, it links the definition of the ADT with their properties, and it makes it impossible for an ADTProperty to contain another ADTType. This I don’t know if it can be solved at all :worried: