/Unityper.jl

Primary LanguageJuliaMIT LicenseMIT

Unityper

Stable Dev Build Status Coverage

Unityper's main capability is to "compactify" structures in static single inheritance. For instance

abstract type AA end
Base.@kwdef struct A2 <: AA
    common_field::Int = 0
    a::Bool = true
    b::Int = 10
end
Base.@kwdef struct B2 <: AA
    common_field::Int = 0
    a::Int = 1
    b::Float64 = 1.0
    d::Complex = 1 + 1.0im # not isbits
end
Base.@kwdef struct C2 <: AA
    common_field::Int = 0
    b::Float64 = 2.0
    d::Bool = false
    e::Float64 = 3.0
    k::Complex{Real} = 1 + 2im # not isbits
end
Base.@kwdef struct D2 <: AA
    common_field::Int = 0
    b::Any = "hi" # not isbits
end

can be compactified by

@compactify begin
    @abstract struct AT
        common_field::Int = 0
    end
    struct A <: AT
        a::Bool = true
        b::Int = 10
    end
    struct B <: AT
        a::Int = 1
        b::Float64 = 1.0
        d::Complex = 1 + 1.0im # not isbits
    end
    struct C <: AT
        b::Float64 = 2.0
        d::Bool = false
        e::Float64 = 3.0
        k::Complex{Real} = 1 + 2im # not isbits
    end
    struct D <: AT
        b::Any = "hi" # not isbits
    end
end

Note that the concrete types A, B, C, and D here are only conceptual, and Unityper compactifies these types into a single AT type. Hence, to check concrete types, ones needs to use Unityper's @compactified macro

foo!(xs) = for i in eachindex(xs)
    @inbounds x = xs[i]
    @inbounds xs[i] = @compactified x::AT begin
        A => D()
        B => A()
        C => B()
        D => A()
    end
end

The above code is equivalent with

goo!(xs) = for i in eachindex(xs)
    @inbounds x = xs[i]
    @inbounds xs[i] = x isa A2 ? D2() :
                      x isa B2 ? A2() :
                      x isa C2 ? B2() :
                      x isa D2 ? A2() : error()
end

Now, let's benchmark these implementations

using Random
rng = Random.MersenneTwister(123)
gs = map(x->rand(rng, (A2(), B2(), C2(), D2())), 1:10000);
rng = Random.MersenneTwister(123)
xs = map(x->rand(rng, (A(), B(), C(), D())), 1:10000);
using BenchmarkTools
@btime foo!($xs);
@btime goo!($gs);

On my laptop, the benchmark result is

julia> @btime foo!($xs);
  58.619 μs (0 allocations: 0 bytes)

julia> @btime goo!($gs);
  116.980 μs (10000 allocations: 312.50 KiB)

Keep in mind that the goo! function is optimal in the sense that it explicitly checks all the sub-types of AA. We can see that Unityper gives a 2x speed up even in the case where the ordinary Julia code is close to optimal.