From eb8d46dffc6e72376b54d7d97a26597275a2efed Mon Sep 17 00:00:00 2001 From: Nathan Daly Date: Fri, 24 Jan 2025 14:47:53 -0700 Subject: [PATCH] Another big improvement to dynamic property access: Fix dynamic dispatch perf (#36) There's a known issue with dynamic dispatch (Thanks to @topolarity to pointing this out) where it's super slow if you dispatch on a type / constructor. Our code had a dispatch on Blob{FT} with an unknown FT in the case of a dynamic field access. That was causing very slow performance. By changing this to a helper function (make_blob), we do a performant dispatch, dramatically improving perf. Now, dynamic field access on a Blob has the ~same~ even better performance as dynamic field access on a regular julia object! :) Before: ```julia julia> @btime ((a)->a[1].y)($(Any[foo])) 115.105 ns (2 allocations: 48 bytes) Blob{Float32}(Ptr{Nothing} @0x000000016fe73d90, 8, 12) ``` After: ```julia julia> @btime ((a)->a[1].y)($(Any[foo])) 48.015 ns (1 allocation: 32 bytes) Blob{Float32}(Ptr{Nothing} @0x000000016fe73d90, 8, 12) # Comparison to getproperty on a normal struct: julia> @btime ((a)->a[1].y)($(Any[Foo(2,3)])) 52.550 ns (2 allocations: 48 bytes) 3.0f0 ``` Follow up to #35. --- src/blob.jl | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/blob.jl b/src/blob.jl index 8ace975..ea4d73e 100644 --- a/src/blob.jl +++ b/src/blob.jl @@ -12,6 +12,14 @@ struct Blob{T} end end +# OPTIMIZATION: Annoyingly julia dispatches on a *Type Constructor* are very expensive. +# So if you ever have a type unstable field access on a Blob, we will not know the type of +# the child Blob we will return, meaning a dynamic dispatch. Dispatching on Blob{FT}(...) +# for an unknown FT is very expensive. So instead, we dispatch to this function, which will +# be cheap because it has only one method. Then this function calls the constructor. +# This is a silly hack. :') +make_blob(::Type{BT}, b::Blob, offset) where {BT} = Blob{BT}(b + offset) + function Blob(ref::Base.RefValue{T}) where T Blob{T}(pointer_from_objref(ref), 0, sizeof(T)) end @@ -169,7 +177,7 @@ end end i = fieldidx_lookup[field] FT = fieldtype(T, i) - Blob{FT}(blob + blob_offset(T, i)) + make_blob(FT, blob, blob_offset(T, i)) end @noinline _throw_missing_field_error(T, field) = error("$T has no field $field") @@ -180,7 +188,8 @@ end @boundscheck if i < 1 || i > fieldcount(T) _throw_getindex_boundserror(blob, i) end - return Blob{fieldtype(T, i)}(blob + Blobs.blob_offset(T, i)) + FT = fieldtype(T, i) + return make_blob(FT, blob, Blobs.blob_offset(T, i)) end Base.@propagate_inbounds function Base.setindex!(blob::Blob{T}, value::T) where T