I think the performance of Dict
is pretty good… and by paying close attention to how it works, the performance can be excellent. Consider this example:
struct MyKey a::Int end
struct MyValue a::Int end
new_record() = MyValue(rand(Int))
d = Dict{MyKey,MyValue}()
A first benchmark yields:
julia> @btime get!(new_record, $d, MyKey(4))
38.907 ns (1 allocation: 16 bytes)
MyValue(3327136004207544748)
Which is quite disappointing. The first optimization to do is to speed up the hash code:
Base.hash(x::MyKey) = x.a % UInt
New benchmark:
julia> @btime get!(new_record, $d, MyKey(4))
8.354 ns (0 allocations: 0 bytes)
MyValue(7805519516943331301)
Better, but not great (24 clock cycles on my machine). Now, there’s a trick we can do to achieve much better lookup speeds, by using the more efficient get
instead of get!
, and wrapping new_record
in its own function:
julia> add_new_record(k) = d[k] = new_record();
julia> get_record!(h, k) = get(() -> add_new_record(k), h, k);
julia> @btime get_record!($d, MyKey(4))
2.253 ns (0 allocations: 0 bytes)
MyValue(7805519516943331301)
2.25 ns, or 6.5 clock cycles… boosted a bit by @btime
(warm caches and branch predictors), but still. This approach is a bit hacky and unflexible however, since it binds the dictionary d
to get_record
(I didn’t find a way to avoid that, while keeping the same performance). If we don’t mind using the internals of Dict
(unfortunately, I often find that I have to), a more flexible approach is to define:
@inline function fast_get!(default::Base.Callable, h::Dict{K,V}, key::K) where {K,V}
index = Base.ht_keyindex(h, key)
index < 0 ? get!(default, h, key) : @inbounds h.vals[index]::V
end
Which results in the same fast lookups:
julia> @btime fast_get!(new_record, $d, MyKey(4))
2.257 ns (0 allocations: 0 bytes)
With this performance, I doubt that there’s a need to try to optimize it further. With some exceptions… memory usage being one of them, or if it’s expensive/difficult to calculate hash codes.