Summary
OptimizationContainer's field containers are type-erased, so every per-timestep read of a variable, parameter, or expression in a consumer's build loop is an ::Any access resolved by dynamic dispatch. This is the dominant type-instability in the optimization stack — every other hot-path finding downstream compounds through it.
Details
src/core/optimization_container.jl:74-80:
variables / aux_variables / duals / constraints / expressions are OrderedDict{<KeyType>, JuMPArray} where JuMPArray = Union{JuMP.Containers.DenseAxisArray, JuMP.Containers.SparseAxisArray} (src/core/definitions.jl:80) — the union carries no element/axis parameters, so retrieval erases everything.
parameters::OrderedDict{ParameterKey, ParameterContainer} (:80) stores ParameterContainer{T,U,A} values with the parameters stripped.
default_time_series_type::Type (:95) is an abstract Type field; every consumer keying off it (get_default_time_series_type call sites in parameter/expression construction) pays a method lookup per call.
Consequence: get_variable / get_parameter / get_expression return values whose type parameters are gone, so indexing like multiplier[name, t] or variable[name, t] inside for t in time_steps loops returns ::Any and every downstream operation redispatches dynamically per (device, timestep).
Measured with a faithful minimal reproduction of the ParameterContainer{T,U,A}-in-erased-dict shape (Julia 1.12.5): Base.return_types gives Any on the read path, and a 200-iteration read loop allocates 6480 bytes vs 0 bytes for the concretely-typed equivalent.
Secondary, same mechanism at lower multiplicity:
Dict{String, AbstractArray} staging in consumers' _add_time_series_parameters!-style code erases the concrete Vector{Float64} the accessors actually return, forcing per-step redispatch into _set_parameter_at! (which itself has clean concrete dispatch arms — the callers hand it Any).
Suggested fix direction
This is architecture-level, not a local patch:
- A typed per-
(EntryType, ComponentType) cache — the dict values become concretely parameterized per key type, function-barrier style, so the container hand-off into the timestep loop specializes; or
- A return-type-asserting barrier at the
get_variable/get_parameter/get_expression boundary (callers state the expected concrete container type), keeping the storage erased but the loop bodies monomorphic; and
- Parameterize the container (or thread a
Type{T}/Val{T} argument) for default_time_series_type instead of the abstract Type field.
Found during a type-stability audit of the time-series chain (details reproducible with @code_warntype on any consumer build-loop read).
Summary
OptimizationContainer's field containers are type-erased, so every per-timestep read of a variable, parameter, or expression in a consumer's build loop is an::Anyaccess resolved by dynamic dispatch. This is the dominant type-instability in the optimization stack — every other hot-path finding downstream compounds through it.Details
src/core/optimization_container.jl:74-80:variables/aux_variables/duals/constraints/expressionsareOrderedDict{<KeyType>, JuMPArray}whereJuMPArray = Union{JuMP.Containers.DenseAxisArray, JuMP.Containers.SparseAxisArray}(src/core/definitions.jl:80) — the union carries no element/axis parameters, so retrieval erases everything.parameters::OrderedDict{ParameterKey, ParameterContainer}(:80) storesParameterContainer{T,U,A}values with the parameters stripped.default_time_series_type::Type(:95) is an abstractTypefield; every consumer keying off it (get_default_time_series_typecall sites in parameter/expression construction) pays a method lookup per call.Consequence:
get_variable/get_parameter/get_expressionreturn values whose type parameters are gone, so indexing likemultiplier[name, t]orvariable[name, t]insidefor t in time_stepsloops returns::Anyand every downstream operation redispatches dynamically per (device, timestep).Measured with a faithful minimal reproduction of the
ParameterContainer{T,U,A}-in-erased-dict shape (Julia 1.12.5):Base.return_typesgivesAnyon the read path, and a 200-iteration read loop allocates 6480 bytes vs 0 bytes for the concretely-typed equivalent.Secondary, same mechanism at lower multiplicity:
Dict{String, AbstractArray}staging in consumers'_add_time_series_parameters!-style code erases the concreteVector{Float64}the accessors actually return, forcing per-step redispatch into_set_parameter_at!(which itself has clean concrete dispatch arms — the callers hand itAny).Suggested fix direction
This is architecture-level, not a local patch:
(EntryType, ComponentType)cache — the dict values become concretely parameterized per key type, function-barrier style, so the container hand-off into the timestep loop specializes; orget_variable/get_parameter/get_expressionboundary (callers state the expected concrete container type), keeping the storage erased but the loop bodies monomorphic; andType{T}/Val{T}argument) fordefault_time_series_typeinstead of the abstractTypefield.Found during a type-stability audit of the time-series chain (details reproducible with
@code_warntypeon any consumer build-loop read).