So the generator is an interesting solution, but I’m going to leave it aside for now. I went ahead and reimplemented the dictionary-lookup method using your basic suggestions:
julia> sLock = ReentrantLock()
julia> sDict = Dict{DataType, Vector}()
julia> function emptytest(::Type{T}) where {T}
lock(sLock)
sTmp = get!(sDict, T) do
Vector{T}()
end::Vector{T}
unlock(sLock)
return sTmp
end
julia> @btime emptytest(String)
86.560 ns (0 allocations: 0 bytes)
0-element Array{String,1}
So I’m still not getting the performance you seem to—does anything look off in my code? (For reference, @btime Vector{Symbol}()
takes 15-16 ns on my system.) If I comment out the lock and unlock functions, the above timing drops to about 45 ns. Regardless, this is about 8 times faster than the implementation I referenced in earlier posts. Somewhat to my surprise, the difference that mattered most seems to be using f(::Type{T}) where {T}
in the function signature instead of just f(T::Type)
.
Additionally, even if the performance were closer to a microsecond, I thought it would still be nice to have this ability for general use, so I wrote a macro:
# This helper function makes sure than a function-arg declaration has a name.
_memconst_fixarg(arg::Expr) = (arg.head == :(::) && length(arg.args) == 1
? Expr(:(::), gensym(), arg.args[1])
: arg)
"""
@memconst name(args...) = expr
@memconst name(args...) where {...} = expr
@memconst is a macro for declaring parameterized constants whose values are
a function of some parameters but which can be memoized after their first
calculation and simply returned after that. To access the value, simply
treat name as a function.
"""
macro memconst(assgn::Expr)
(assgn.head == :(=)) || throw(
ArgumentError("memconst must be given an assignment expression"))
# Parse the assignment statement.
lhs = assgn.args[1]
expr = assgn.args[2]
if lhs.head == :call
fsym = lhs.args[1]
args = [_memconst_fixarg(a) for a in lhs.args[2:end]]
lhs = Expr(:call, fsym, args...)
elseif lhs.head == :where
fsig = lhs.args[1]
fsym = fsig.args[1]
args = [_memconst_fixarg(a) for a in fsig.args[2:end]]
fsig = Expr(:call, fsym, args...)
lhs = Expr(:where, fsig, lhs.args[2:end]...)
else
throw(ArgumentError("memconst assignment LHS must be a call or where expression"))
end
# Make an expression for the tuple of arguments.
argtup = Expr(:tuple, args...)
# Symbols we will need in the generated code.
sDict = gensym()
sLock = gensym()
sTmp = gensym()
quote
$sLock = ReentrantLock()
$sDict = Dict{Tuple, Any}()
$lhs = begin
lock($sLock)
$sTmp = get!($sDict, $argtup) do; $expr end
unlock($sLock)
return $sTmp
end
end |> esc
end
This implementation runs pretty fast:
julia> @memconst emptyVecTest(::Type{T}) where {T} = Vector{T}()
emptyVecTest (generic function with 1 method)
julia> @btime emptyVecTest(String)
60.052 ns (1 allocation: 16 bytes)
0-element Array{Symbol,1}
Also, this macro can be used with multi-argument parameterized constants.
julia> @memconst emptyDictTest(::Type{K}, ::Type{V}) where {K,V} = Dict{K,V}()
emptyDictTest (generic function with 1 method)
julia> @btime emptyDictTest(Symbol, Int)
68.722 ns (1 allocation: 32 bytes)
Dict{Symbol,Int64} with 0 entries
I think this is basically a win, but I’m curious if anyone sees opportunity to shave a few more nanoseconds off.