Skip to content

OptimizationContainer's type-erased storage forces per-timestep dynamic dispatch in consumer build loops #155

Description

@jd-lara

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:

  1. 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
  2. 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
  3. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions