Benchmarks
We benchmark the performance of Moshi against other packages for the a typical use case
from the README of Unityper. This benchmark has been the baseline benchmark for all
the packages that we have compared against.
Results
Section titled “Results”We see that Moshi is the fastest among all the packages that is almost equivalent to the baseline implementation (~1x comparing to baseline speed) in both allocation size and matching speed.
The benchmark was run on a machine with the following configuration:
julia> versioninfo()Julia Version 1.10.4Commit 48d4fd48430 (2024-06-04 10:41 UTC)Build Info: Official https://julialang.org/ releasePlatform Info: OS: macOS (arm64-apple-darwin22.4.0) CPU: 10 × Apple M1 Pro WORD_SIZE: 64 LIBM: libopenlibm LLVM: libLLVM-15.0.7 (ORCJIT, apple-m1)Threads: 8 default, 0 interactive, 4 GC (on 8 virtual cores)Environment: JULIA_EDITOR = codeBenchmarking Code
Section titled “Benchmarking Code”The benchmarking code uses the same convention and are wrapped in a module. The detailed implementations are as follows:
This is with the normal @match macro.
module MoshiMatchBench
using Randomusing Moshi.Data: Data, @data, isa_variantusing Moshi.Match: @match
@data AT begin struct A common_field::Int = 0 a::Bool = true b::Int = 10 end struct B common_field::Int = 0 a::Int = 1 b::Float64 = 1.0 d::Complex = 1 + 1.0im # not isbits end struct C common_field::Int = 0 b::Float64 = 2.0 d::Bool = false e::Float64 = 3.0 k::Complex{Real} = 1 + 2im # not isbits end struct D common_field::Int = 0 b::Any = "hi" # not isbits endend
function generate(len::Int) return rand(Random.MersenneTwister(123), (AT.A(), AT.B(), AT.C(), AT.D()), len)end
function main!(xs) @inbounds for i in eachindex(xs) xs[i] = @match xs[i] begin AT.A(cf, a, b) => AT.B(cf + 1, a, b, b) AT.B(cf, a, b, d) => AT.C(cf - 1, b, isodd(a), b, d) AT.C(cf) => AT.D(cf + 1, isodd(cf) ? "hi" : "bye") AT.D(cf, b) => AT.A(cf - 1, b == "hi", cf) end endend
end # moduleThis is using Moshi’s reflections.
module MoshiBench
using Randomusing Moshi.Data: Data, @data, isa_variant
@data AT begin struct A common_field::Int = 0 a::Bool = true b::Int = 10 end struct B common_field::Int = 0 a::Int = 1 b::Float64 = 1.0 d::Complex = 1 + 1.0im # not isbits end struct C common_field::Int = 0 b::Float64 = 2.0 d::Bool = false e::Float64 = 3.0 k::Complex{Real} = 1 + 2im # not isbits end struct D common_field::Int = 0 b::Any = "hi" # not isbits endend
function generate(len::Int) return rand(Random.MersenneTwister(123), (AT.A(), AT.B(), AT.C(), AT.D()), len)end
function main!(xs) for i in eachindex(xs) x = xs[i] xs[i] = if isa_variant(x, AT.A) AT.B(x.common_field + 1, x.a, x.b, x.b) elseif isa_variant(x, AT.B) AT.C(x.common_field - 1, x.b, isodd(x.a), x.b, x.d) elseif isa_variant(x, AT.C) AT.D(x.common_field + 1, isodd(x.common_field) ? "hi" : "bye") else AT.A(x.common_field - 1, x.b == "hi", x.common_field) end endend
end # MoshiBenchThis is using Moshi’s variant_getfield reflection, which is equivalent to the pattern matching.
module MoshiHackyBench
using Randomusing Moshi.Data: Data, @data, isa_variant, variant_getfieldusing Moshi.Match: @match
@data AT begin struct A common_field::Int = 0 a::Bool = true b::Int = 10 end struct B common_field::Int = 0 a::Int = 1 b::Float64 = 1.0 d::Complex = 1 + 1.0im # not isbits end struct C common_field::Int = 0 b::Float64 = 2.0 d::Bool = false e::Float64 = 3.0 k::Complex{Real} = 1 + 2im # not isbits end struct D common_field::Int = 0 b::Any = "hi" # not isbits endend
function generate(len::Int) return rand(Random.MersenneTwister(123), (AT.A(), AT.B(), AT.C(), AT.D()), len)end
function main!(xs) @inbounds for i in eachindex(xs) x = xs[i] xs[i] = if isa_variant(x, AT.A) cf = Data.variant_getfield(x, AT.A, :common_field) a = Data.variant_getfield(x, AT.A, :a) b = Data.variant_getfield(x, AT.A, :b) AT.B(cf + 1, a, b, b) elseif isa_variant(x, AT.B) cf = Data.variant_getfield(x, AT.B, :common_field) a = Data.variant_getfield(x, AT.B, :a) b = Data.variant_getfield(x, AT.B, :b) d = Data.variant_getfield(x, AT.B, :d) AT.C(cf - 1, b, isodd(a), b, d) elseif isa_variant(x, AT.C) cf = Data.variant_getfield(x, AT.C, :common_field) AT.D(cf + 1, isodd(cf) ? "hi" : "bye") else cf = Data.variant_getfield(x, AT.D, :common_field) b = Data.variant_getfield(x, AT.D, :b) AT.A(cf - 1, b == "hi", cf) end endend
end # modulemodule ExproniconBench
using Randomusing Expronicon.ADT: @adtusing MLStyle: @match
@adt AT begin struct A common_field::Int = 0 a::Bool = true b::Int = 10 end struct B common_field::Int = 0 a::Int = 1 b::Float64 = 1.0 d::Complex = 1 + 1.0im # not isbits end struct C common_field::Int = 0 b::Float64 = 2.0 d::Bool = false e::Float64 = 3.0 k::Complex{Real} = 1 + 2im # not isbits end struct D common_field::Int = 0 b::Any = "hi" # not isbits endend
function generate(len::Int) return rand(Random.MersenneTwister(123), (AT.A(), AT.B(), AT.C(), AT.D()), len)end
function main!(xs) for i in eachindex(xs) @inbounds x = xs[i] @inbounds xs[i] = @match x begin AT.A(cf, a, b) => AT.B(cf + 1, a, b, b) AT.B(cf, a, b, d) => AT.C(cf - 1, b, isodd(a), b, d) AT.C(cf, b, d, e, k) => AT.D(cf + 1, isodd(cf) ? "hi" : "bye") AT.D(cf, b) => AT.A(cf - 1, b == "hi", cf) end endend
end # ExproniconBenchmodule SumTypeTest
using Randomusing SumTypes, BenchmarkTools@sum_type AT begin A(common_field::Int, a::Bool, b::Int) B(common_field::Int, a::Int, b::Float64, d::Complex) C(common_field::Int, b::Float64, d::Bool, e::Float64, k::Complex{Real}) D(common_field::Int, b::Any)end
function generate(len::Int) return rand( Random.MersenneTwister(123), ( A(0, true, 10), B(0, 1, 1.0, 1 + 1im), C(0, 2.0, false, 3.0, Complex{Real}(1 + 2im)), D(0, "hi"), ), len, )end
function main!(xs) for i in eachindex(xs) xs[i] = @cases xs[i] begin A(cf, a, b) => B(cf + 1, a, b, b) B(cf, a, b, d) => C(cf - 1, b, isodd(a), b, d) C(cf, b, d, e, k) => D(cf + 1, isodd(cf) ? "hi" : "bye") D(cf, b) => A(cf - 1, b == "hi", cf) end endend # main!
end # modulemodule LightSumTypesBench
using Randomusing LightSumTypes
@kwdef struct A common_field::Int = 0 a::Bool = true b::Int = 10end@kwdef struct B common_field::Int = 0 c::Int = 1 d::Float64 = 1.0 e::Complex = 1.0 + 1.0imend@kwdef struct C common_field::Int = 0 f::Float64 = 2.0 g::Bool = false h::Float64 = 3.0 i::Complex{Real} = 1 + 2imend@kwdef struct D common_field::Int = 0 l::Any = "hi"end
@sumtype AT(A, B, C, D)
function generate(len::Int) return rand(MersenneTwister(123), (AT(A()), AT(B()), AT(C()), AT(D())), len)end
function main!(xs) for i in eachindex(xs) @inbounds xs[i] = main_each(variant(xs[i])) endend
main_each(x::A) = AT(B(x.common_field + 1, x.a, x.b, x.b))main_each(x::B) = AT(C(x.common_field - 1, x.d, isodd(x.c), x.d, x.e))main_each(x::C) = AT(D(x.common_field + 1, isodd(x.common_field) ? "hi" : "bye"))main_each(x::D) = AT(A(x.common_field - 1, x.l == "hi", x.common_field))
end # modulemodule UnityperBench
using Randomusing Unityper
@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 endend
function generate(len::Int) rng = Random.MersenneTwister(123) return rand(rng, (A(), B(), C(), D()), len)end
function main!(xs) for i in eachindex(xs) @inbounds x = xs[i] @inbounds xs[i] = @compactified x::AT begin A => B(; common_field=x.common_field + 1, a=x.a, b=x.b, d=x.b) B => C(; common_field=x.common_field - 1, b=x.b, d=isodd(x.a), e=x.b, k=x.d) C => D(; common_field=x.common_field + 1, b=isodd(x.common_field) ? "hi" : "bye") D => A(; common_field=x.common_field - 1, a=x.b == "hi", b=x.common_field) end endend
end # module