I want to have a few buffers and/or dicts that serve as caches for various methods I’m implementing on my struct. This struct will basically be alive for the lifetime of the program as it’s a wrapper containing information about an MCMC simulation. I was wondering which of these two methods is more GC friendly?
Method 1: Dictionary of buffers
mutable struct Lattice{BuffType, DictType}
# ...configuration and state fields...
cache::Dict{Symbol, Union{BuffType, DictType}}
end
And then to get a specific buffer and/or dict. I can just index into it. The problem with this would be that BuffType
and DictType
need not be the same for the various caches I require. For instance, one cache has type Vector{Tuple{Int, Tuple{Float32, Float32}, Tuple{Float32, Float32}}}
while another is just Vector{Float32}
.
Method 2 - Dictionary of closures that capture buffers
mutable struct Lattice
# ...configuration and state fields...
cache::Dict{Symbol, Function}
end
L = Lattice(Dict{Symbol, Function}())
function complicatedcachegen(L::Lattice)
# Computes size of cache using configuration in L
cachesize = 10
c = Vector{Tuple{ Int, Tuple{Float32, Float32}, Tuple{Float32, Float32} }}(undef, cachesize)
# Capture `c` for future use.
return () -> c
end
L.cache[:complicated] = complicatedcachegen(L)
# Can be used later like so
complicatedcache = L.cache[:complicated]()
This solves the problem of different types of caches while making sure that everything in the struct has a concrete type. My question would be - is this memory/GC friendly? Or is this a footgun for memory and/or performance? If I try to use this cache in a function, I get a few ::Any
types in @code_warntype
.
julia> function usecache(L::Lattice)
c = L.cache[:complicated]()
c[1] = (1, (2., 3.), (4., 5.))
c[2] = (2, (3., 4.), (5., 6.))
return c[1:2]
end
julia> @code_warntype usecache(L)
MethodInstance for usecache(::Lattice)
from usecache(L::Lattice) @ Main REPL[5]:1
Arguments
#self#::Core.Const(Main.usecache)
L::Lattice
Locals
c::Any
Body::Any
1 ─ %1 = Base.getproperty(L, :cache)::Dict{Symbol, Function}
│ %2 = Base.getindex(%1, :complicated)::Function
│ (c = (%2)())
│ %4 = Core.tuple(2.0, 3.0)::Core.Const((2.0, 3.0))
│ %5 = Core.tuple(4.0, 5.0)::Core.Const((4.0, 5.0))
│ %6 = Core.tuple(1, %4, %5)::Core.Const((1, (2.0, 3.0), (4.0, 5.0)))
│ %7 = c::Any
│ Base.setindex!(%7, %6, 1)
│ %9 = Core.tuple(3.0, 4.0)::Core.Const((3.0, 4.0))
│ %10 = Core.tuple(5.0, 6.0)::Core.Const((5.0, 6.0))
│ %11 = Core.tuple(2, %9, %10)::Core.Const((2, (3.0, 4.0), (5.0, 6.0)))
│ %12 = c::Any
│ Base.setindex!(%12, %11, 2)
│ %14 = c::Any
│ %15 = Main.:(:)::Core.Const(Colon())
│ %16 = (%15)(1, 2)::Core.Const(1:2)
│ %17 = Base.getindex(%14, %16)::Any
└── return %17
Is the compiler unable to determine the type of c
even though it’s concretely defined? Should I be worried?
Edit: Here’s a simpler working example where the Body::Any
situation comes up.
julia> function A()
x = Vector{Tuple{Int, Tuple{Int, Int}}}(undef, 4)
return () -> x
end
A (generic function with 1 method)
julia> @code_warntype A()
MethodInstance for A()
from A() @ Main REPL[15]:1
Arguments
#self#::Core.Const(Main.A)
Locals
#7::var"#7#8"{Vector{Tuple{Int64, Tuple{Int64, Int64}}}}
x::Vector{Tuple{Int64, Tuple{Int64, Int64}}}
Body::var"#7#8"{Vector{Tuple{Int64, Tuple{Int64, Int64}}}}
1 ─ %1 = Main.Vector::Core.Const(Vector)
│ %2 = Main.Tuple::Core.Const(Tuple)
│ %3 = Main.Int::Core.Const(Int64)
│ %4 = Core.apply_type(Main.Tuple, Main.Int, Main.Int)::Core.Const(Tuple{Int64, Int64})
│ %5 = Core.apply_type(%2, %3, %4)::Core.Const(Tuple{Int64, Tuple{Int64, Int64}})
│ %6 = Core.apply_type(%1, %5)::Core.Const(Vector{Tuple{Int64, Tuple{Int64, Int64}}})
│ %7 = Main.undef::Core.Const(UndefInitializer())
│ (x = (%6)(%7, 4))
│ %9 = Main.:(var"#7#8")::Core.Const(var"#7#8")
│ %10 = x::Vector{Tuple{Int64, Tuple{Int64, Int64}}}
│ %11 = Core.typeof(%10)::Core.Const(Vector{Tuple{Int64, Tuple{Int64, Int64}}})
│ %12 = Core.apply_type(%9, %11)::Core.Const(var"#7#8"{Vector{Tuple{Int64, Tuple{Int64, Int64}}}})
│ %13 = x::Vector{Tuple{Int64, Tuple{Int64, Int64}}}
│ (#7 = %new(%12, %13))
│ %15 = #7::var"#7#8"{Vector{Tuple{Int64, Tuple{Int64, Int64}}}}
└── return %15
julia> D = Dict{Symbol, Function}(:A => A())
Dict{Symbol, Function} with 1 entry:
:A => #7
Using the cache outside a function vs inside, we have
julia> @code_warntype D[:A]()
MethodInstance for (::var"#7#8"{Vector{Tuple{Int64, Tuple{Int64, Int64}}}})()
from (::var"#7#8")() @ Main REPL[15]:3
Arguments
#self#::var"#7#8"{Vector{Tuple{Int64, Tuple{Int64, Int64}}}}
Body::Vector{Tuple{Int64, Tuple{Int64, Int64}}}
1 ─ %1 = Core.getfield(#self#, :x)::Vector{Tuple{Int64, Tuple{Int64, Int64}}}
└── return %1
julia> @code_warntype ( () -> D[:A]()[1:2] )()
MethodInstance for (::var"#9#10")()
from (::var"#9#10")() @ Main REPL[23]:1
Arguments
#self#::Core.Const(var"#9#10"())
Body::Any
1 ─ %1 = Base.getindex(Main.D, :A)::Any
│ %2 = (%1)()::Any
│ %3 = (1:2)::Core.Const(1:2)
│ %4 = Base.getindex(%2, %3)::Any
└── return %4
julia> @code_warntype ( (d) -> d[:A]()[1:2] )(D)
MethodInstance for (::var"#11#12")(::Dict{Symbol, Function})
from (::var"#11#12")(d) @ Main REPL[24]:1
Arguments
#self#::Core.Const(var"#11#12"())
d::Dict{Symbol, Function}
Body::Any
1 ─ %1 = Base.getindex(d, :A)::Function
│ %2 = (%1)()::Any
│ %3 = (1:2)::Core.Const(1:2)
│ %4 = Base.getindex(%2, %3)::Any
└── return %4
Any way to make the last two calls to D[:A]()
type stable?