We have an expensive algorithm with is run many, many times for different parameter values and want to design the interface for caching the memory preallocation. A few principles:
- We should have enough information to create the cache prior to running it through the first time, it would like to reuse allocations made for the model solutions as well.
- We need to support 2 variations of the algorithms for dense vs. sparse values. The cache would need to support this
- Performance really matters, and the sizes of these might be very large
- It would be nice to have a convenience version of the function when you don’t want to worry about any caches.
In order to implement this, I came up with the following MWE to get across the features we need for generic types, caches, etc. I would love feedback on the design conventions, hidden gotchas for performance hits, etc. Obvoiusly N=2
is just for the example and I have been too lazy to constrain the types of the generic types
using SparseArrays, Parameters, LinearAlgebra, Statistics
struct MyModel
is_sparse::Bool
N::Int64
end
struct MySolverCache{T}
temp::T # e.g. Array{Float64,2}
end
MySolverCache(m::MyModel) = MySolverCache(m, Val(m.is_sparse))
MySolverCache(m::MyModel, is_sparse::Val{false}) = MySolverCache(Array{Float64,2}(undef, m.N, m.N)) # cache for dense
MySolverCache(m::MyModel, is_sparse::Val{true}) = MySolverCache(spzeros(m.N, m.N)) # cache for sparse
mutable struct MySolution{T, T2}
vec_val::T
array_val::T2 # e.g. Array{Float64,2}
val::Float64
end
# Constructors to allocate model solution caches
MySolution(m::MyModel) = MySolution(m, Val(m.is_sparse))
MySolution(m::MyModel, is_sparse::Val{false}) = MySolution(Array{Float64,1}(undef, m.N), Array{Float64,2}(undef, m.N, m.N), 0.0)
MySolution(m::MyModel, is_sparse::Val{true}) = MySolution(Array{Float64,1}(undef, m.N), spzeros(m.N, m.N), 0.0)
function make_model(N; is_sparse = false, allocate_cache = false)
m = MyModel(is_sparse, N)
return allocate_cache ? (m, MySolverCache(m), MySolution(m)) : m
end
function solve_model!(x, m::MyModel, solver_cache::MySolverCache = MySolverCache(m), sol::MySolution = MySolution(m))
@unpack N = m
@unpack temp = solver_cache
@unpack vec_val, array_val = sol
temp .= 0.0 # clears cache if we needed to... works on sparse and dense.
# some calculations...
temp[1,1] = x
temp[1,2] = 3.0
temp[2,1] = 1.0
vec_val .= Array(diag(temp)) # return a vector
array_val .= temp # return a matrix
array_val[1,1] += 2.0
sol.val = mean(vec_val)
return sol
end
# usage
m, solver_cache, sol = make_model(2, is_sparse = true, allocate_cache = true) # allocates and sparse
solve_model!(0.5, m, solver_cache, sol)
solve_model!(0.2, m, solver_cache, sol) # would reuse the cache
m, solver_cache, sol = make_model(2, allocate_cache = true) # allocates and dense
solve_model!(0.5, m, solver_cache, sol)
solve_model!(0.2, m, solver_cache, sol)
m = make_model(2) # no allocations, dense
solution = solve_model!(0.5, m) # no pre-allocation