Caching the results of a factory method

In the code snippet below the function make_foo() returns either Bar or Baz depending on the value of its input. In the actual code the determination of the type and actual construction of the result include some long computations; the type stability is therefore not an issue. The results are then stored in a collection, and due to performance considerations I prefer to store the results in two separate arrays (Vector{Bar} and Vector{Baz}) rather than putting everything in a Vector{Union{Bar,Baz}}. In the same time, it is desirable to cache the position of the result in the collection as a function of input. In the code below this is achieved by storing the position in a dictionary with the values of type Tuple{DataType, Int}, where the first element always equals either Bar or Baz, and the second is the index in the corresponding array. However, I have an impression that this is not quite a julian way of doing things…

Is there a better approach to this problem?

module Scratch

struct Bar
    x::Int
end

struct Baz
    x::Int
end

const Foo=Union{Bar,Baz}

make_foo(x::Int)::Foo = (x%2==0) ? Bar(x) : Baz(x)

struct Data
    bar::Vector{Bar}
    baz::Vector{Baz}
    dic::Dict{Int,Tuple{DataType,Int}}
end

Data()=Data(Vector{Bar}(), Vector{Baz}(), Dict{Int,Tuple{DataType,Int}}())

function _add!(d::Data, b::Bar)
    push!(d.bar, b)
    length(d.bar)
end

function _add!(d::Data, b::Baz)
    push!(d.baz, b)
    length(d.baz)
end

function add!(d::Data, f::Foo)
    haskey(d.dic, f.x) && throw(DomainError("Duplicate data"))
    n=_add!(d,f)
    d.dic[f.x]=(typeof(f),n)
end

function main()
    d=Data()
    for i=1:10
        add!(d, make_foo(i))
    end
    d
end

end

This seems like a pretty complicated way of essentially doing the same things that a Vector{Union{Bar, Baz}} would do. Do you have a particular use case or benchmark where the union vector doesn’t perform well enough? It’s easier to reason about performance issues with a measurable target.

1 Like

Well, benchmarks are not ready yet. Note, however, that neither the calls of the factory nor accessing the cache are meant to be performed frequently; however, the stored data are accessed in a rather tight loop - this is the main reason to store Bar’s and Baz’s separately.

I should probably try a heterogenous array though. Finally, “premature optimization is the root of all evil”.

If your hot loop consists of iterating over all of the elements of both types, then this might be an excellent use case for https://github.com/tkoolen/TypeSortedCollections.jl . But yes, I would definitely suggest choosing the simplest solution until you have a benchmark that indicates a need for change.

3 Likes

Yes, it seems that TypeSortedCollections.jl is exactly what is needed. Thank you for this information!