Open AntonOresten opened 2 weeks ago
I think that anyway a good benchmark between static and dynamic properties could be:
julia> using DynamicStructs
julia> @dynamic mutable struct A x::Int end
julia> v = [A(i, y=i) for i in 1:1000];
julia> using BenchmarkTools
julia> @benchmark sum(a.x for a in $v)
BenchmarkTools.Trial: 10000 samples with 181 evaluations.
Range (min … max): 585.221 ns … 697.099 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 593.862 ns ┊ GC (median): 0.00%
Time (mean ± σ): 596.373 ns ± 6.762 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
▁▁ ▂▇█▇▄ ▁▃▄▃ ▁ ▂▂ ▂
▄▅▇▅▃▁▁▆███▆▅▄█████▆▄▅▃▄▃▄▄▄▅▄▄▆▄▄▄▆▆▄▄▇█████▆▆▆▆▄▃███▅▅▆▃███ █
585 ns Histogram: log(frequency) by time 617 ns <
Memory estimate: 0 bytes, allocs estimate: 0.
julia> @benchmark sum(a.y for a in $v)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
Range (min … max): 16.832 μs … 1.579 ms ┊ GC (min … max): 0.00% … 96.63%
Time (median): 18.025 μs ┊ GC (median): 0.00%
Time (mean ± σ): 18.872 μs ± 24.117 μs ┊ GC (mean ± σ): 2.12% ± 1.66%
▄▄▅▇██▆▅▄▂ ▁ ▁▁▁▁ ▂
▆▇███████████▆▃▃▁▄▃▃▅▆▃▆▇▇███▇▅▆▆▄▅▄▇▇▇▆▆▆▆▅▄▅▇███████▆▆▅▇▇ █
16.8 μs Histogram: log(frequency) by time 26.9 μs <
Memory estimate: 15.14 KiB, allocs estimate: 969.
Do you have any idea to improve the dynamic benchmark? To me it seems maybe possible if we assume that y
will always be a Int
for all instances. We could enforce it by keeping track on the type of each new added property. At the same time, this restricts a bit the functioning of the library...and I don't still know how to say to the compiler that this can be assumed, but it seems to work:
julia> @benchmark sum(a.y::Int for a in $v)
BenchmarkTools.Trial: 10000 samples with 9 evaluations.
Range (min … max): 2.734 μs … 5.116 μs ┊ GC (min … max): 0.00% … 0.00%
Time (median): 2.805 μs ┊ GC (median): 0.00%
Time (mean ± σ): 2.817 μs ± 78.998 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
▃▃▃▃▃▃▄▇██▆▃▂▁ ▁ ▁▁▁ ▁▁ ▂
▄████████████████▅▆▆▄▅▇█████▇▇▇▅▄▄▃▃▁▄▃▁▃▁▃▄▄▁▃▅▄▆▄▆▇▆████ █
2.73 μs Histogram: log(frequency) by time 3.12 μs <
Memory estimate: 0 bytes, allocs estimate: 0.
We could probably have two different version if we manage to exploit the constrained one for performance, still unsure how though
julia> @benchmark sum(a.y::Int for a in $v)
BenchmarkTools.Trial: 10000 samples with 9 evaluations.
Range (min … max): 2.833 μs … 40.378 μs ┊ GC (min … max): 0.00% … 0.00%
Time (median): 3.711 μs ┊ GC (median): 0.00%
Time (mean ± σ): 3.944 μs ± 978.648 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
▂█▁
▁▁▁▁▁▃▂▃███▄▂▂▃▂▃▅▆▄▂▂▂▂▃▄▃▂▃▄▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁ ▂
2.83 μs Histogram: frequency by time 6.13 μs <
Memory estimate: 0 bytes, allocs estimate: 0.
julia> @benchmark for a in $v
a.y::Int
end
BenchmarkTools.Trial: 10000 samples with 9 evaluations.
Range (min … max): 2.533 μs … 50.122 μs ┊ GC (min … max): 0.00% … 0.00%
Time (median): 3.622 μs ┊ GC (median): 0.00%
Time (mean ± σ): 3.884 μs ± 1.256 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
█
█▁▁▁▁▁▁▃▂▃▄▃▆▃▄▄▃▂▂▂▁▂▂▂▁▂▂▂▃▃▄▄▃▄▄▃▃▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▂
2.53 μs Histogram: frequency by time 6.57 μs <
Memory estimate: 0 bytes, allocs estimate: 0.
julia> @benchmark for a in $v
a.y
end
BenchmarkTools.Trial: 10000 samples with 9 evaluations.
Range (min … max): 2.544 μs … 5.556 μs ┊ GC (min … max): 0.00% … 0.00%
Time (median): 2.567 μs ┊ GC (median): 0.00%
Time (mean ± σ): 2.592 μs ± 84.690 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
▃█ ▇ ▃ ▂ ▂▁ ▁ ▂ ▃ ▃▂ ▂ ▂ ▁ ▂▃ ▂ ▁ ▁ ▂
██▁█▁█▁█▁██▁█▁█▁█▁██▁█▁█▁█▁██▁█▁█▁█▁█▁█▇▁▇▁▇▁▄▁▆▇▁▆▁▆▁▅▁▆▅ █
2.54 μs Histogram: log(frequency) by time 2.9 μs <
Memory estimate: 0 bytes, allocs estimate: 0.
We're bottlenecked by accessing the properties through the dictionary. The type assertion helps a lot in the sum example when we need to dispatch on the type, but we can't escape the inefficiencies of dictionary look-ups, regardless of implementation, especially with Any
values.
It might be possible to use generated functions to route getproperty
to some special function that dispatches on Val{property_name}
with a consistent return type. I'm not sure though, and it might restrict type freedom.
From some preliminary tests I found that functions that accessed regular typed fields of dynamic structs compiled to more or less the same LLVM code as they would if they were regular structs:
For dynamic properties however, Julia needs to access a dictionary, where the value type is always unknown, and do a lot more gymnastics to execute the function:
This overhead is arguably warranted since dynamic properties offer complete freedom.
My thoughts are: Any task that could be bottlenecked by type-instability from accessing dynamic properties should not be using dynamic properties.
This should still be tested and benchmarked more robustly, as well as automated in the CI.