Performance

Static fields

A comparison between accessing static fields of regular and dynamic structs shows that performance impact is minimal, and the LLVM code of accessing fields of a dynamic struct is still "nice".

julia> using DynamicStructs, Chairmarks, InteractiveUtils
julia> struct A x::Int end
julia> @dynamic struct B x::Int end
julia> a, b = A(1), B(1)(Main.A(1), Main.B(1))
julia> f(arg) = arg.x^2f (generic function with 1 method)
julia> @b f($a)2.775 ns
julia> @b f($b)2.774 ns
julia> @code_llvm f(a); Function Signature: f(Main.A) ; @ REPL[5]:1 within `f` define i64 @julia_f_8187(ptr nocapture noundef nonnull readonly align 8 dereferenceable(8) %"arg::A") #0 { top: ; ┌ @ intfuncs.jl:370 within `literal_pow` ; │┌ @ int.jl:88 within `*` %"arg::A.x_ptr.unbox" = load i64, ptr %"arg::A", align 8 %0 = mul i64 %"arg::A.x_ptr.unbox", %"arg::A.x_ptr.unbox" ret i64 %0 ; └└ }
julia> @code_llvm f(b); Function Signature: f(Main.B) ; @ REPL[5]:1 within `f` define i64 @julia_f_8270(ptr nocapture noundef nonnull readonly align 8 dereferenceable(16) %"arg::B") #0 { top: ; ┌ @ /home/runner/work/DynamicStructs.jl/DynamicStructs.jl/src/dynamic.jl:152 within `getproperty` ; │┌ @ /home/runner/work/DynamicStructs.jl/DynamicStructs.jl/src/dynamic.jl:47 within `dynamic_getproperty` %"arg::B.x_ptr" = getelementptr inbounds { ptr, i64 }, ptr %"arg::B", i64 0, i32 1 ; └└ ; ┌ @ intfuncs.jl:370 within `literal_pow` ; │┌ @ int.jl:88 within `*` %"arg::B.x_ptr.unbox" = load i64, ptr %"arg::B.x_ptr", align 8 %0 = mul i64 %"arg::B.x_ptr.unbox", %"arg::B.x_ptr.unbox" ret i64 %0 ; └└ }

Dynamic properties

Dynamic structs build on the static fields of regular structs, essentially strapping on an AbstractDict{Symbol,Any}, which gets accessed if the property is not a field. This flexibility makes for a convenient interface, but is also inherently type-unstable, meaning they will perform similarly to a field of unspecified type. The LLVM code generated from dynamic property access is however significantly longer.

julia> struct C
           x::Int
           y
       end
julia> b, c = B(1, y=2), C(1, 2);
julia> g(arg) = arg.y^2;
julia> @b g($b)21.649 ns
julia> @b g($c)19.435 ns

Tips

Type assertions for type stability and improved performance

julia> v = [B(i, y=i) for i in 1:100];
julia> @b sum(b.x for b in $v)69.390 ns
julia> @b sum(b.y for b in $v)1.917 μs (69 allocs: 1.078 KiB)
julia> @b sum(b.y::Int for b in $v)203.504 ns

Function barriers help... sometimes

We observe that in one scenario, broadcasted addition of short vectors is 2x faster with a function barrier, but 2x slower for longer vectors. This happens also with Any-typed fields of regular structs.

julia> @dynamic struct D end
julia> f(a, b) = a.x .+ a.x;
julia> g(x1, x2) = x1 .+ x2;
julia> f_barrier(a, b) = g(a.x, b.x);
julia> d = D(x = rand(1));
julia> @b f($d, $d)55.820 ns (3 allocs: 96 bytes)
julia> @b f_barrier($d, $d)39.295 ns (2 allocs: 64 bytes)
julia> d = D(x = rand(10000));
julia> @b f($d, $d)1.893 μs (4 allocs: 78.219 KiB)
julia> @b f_barrier($d, $d)1.873 μs (3 allocs: 78.188 KiB)