Performance of keeping a model "reference"

I have asked about the related idea behind my problem over here, but since no conclusion could be drawn, I’ve constructed a little test. That’s where things got weird - I am unsure why the result looks the way they do, and now feel like I am doing something wrong.

Short explanation, the full code is posted below. Basically I am looking to integrate a “reference” to the current JuMP.Model being built into every component (the “components” are some kind of building blocks of this model). That’s why I am comparing the performance of

  1. keeping that as field of the struct
  2. not keeping it at all
  3. keeping it as field of the struct but only as Ref

The initial test showed that 2. resulted in two allocations less than 1. (kind of expected?), while 3. results in three allocations more (did not expect that?).

Now, I am adding all components to the .ext dictionary, and I thought that would not influence the 3 different ways, but something strange happens: 1. and 2. result in the same amount of allocations if I do not save the components into .ext. And now I am completely lost on why this happens…

Really glad for any insights (probably about me misunderstanding how Julia handles something…)!


Allocations:

  1. 209609
  2. 207609
  3. 212609

Allocations without .ext:

  1. 202119
  2. 202119

MWE:

using JuMP
using BenchmarkTools


Base.@kwdef mutable struct Component1
    model::JuMP.Model
    var::Union{Vector{JuMP.VariableRef}, Nothing} = nothing
    bound::Float64
end

Base.@kwdef mutable struct Component2
    var::Union{Vector{JuMP.VariableRef}, Nothing} = nothing
    bound::Float64
end

Base.@kwdef mutable struct Component3
    model::Ref{JuMP.Model}
    var::Union{Vector{JuMP.VariableRef}, Nothing} = nothing
    bound::Float64
end

function foo1()
    m = JuMP.Model()
    set_string_names_on_creation(m, false)

    m.ext[:components] = [Component1(model=m, bound=rand()) for _ in 1:1000]

    for comp in m.ext[:components]
        comp.var = @variable(comp.model, [t=1:100], upper_bound=comp.bound)
    end
end

function foo2()
    m = JuMP.Model()
    set_string_names_on_creation(m, false)

    m.ext[:components] = [Component2(bound=rand()) for _ in 1:1000]

    for comp in m.ext[:components]
        comp.var = @variable(m, [t=1:100], upper_bound=comp.bound)
    end
end

function foo3()
    m = JuMP.Model()
    set_string_names_on_creation(m, false)

    m.ext[:components] = [Component3(model=Ref(m), bound=rand()) for _ in 1:1000]

    for comp in m.ext[:components]
        comp.var = @variable(comp.model[], [t=1:100], upper_bound=comp.bound)
    end
end

function foo1_alt()
    m = JuMP.Model()
    set_string_names_on_creation(m, false)

    comps = [Component1(model=m, bound=rand()) for _ in 1:1000]

    for comp in comps
        comp.var = @variable(comp.model, [t=1:100], upper_bound=comp.bound)
    end
end

function foo2_alt()
    m = JuMP.Model()
    set_string_names_on_creation(m, false)

    comps = [Component2(bound=rand()) for _ in 1:1000]

    for comp in comps
        comp.var = @variable(m, [t=1:100], upper_bound=comp.bound)
    end
end

@benchmark foo1()
@benchmark foo2()
@benchmark foo3()

@benchmark foo1_alt()
@benchmark foo2_alt()

Every variable already keeps a reference to the Model, so storing an additional one should have minimal impact. (If you have a variable, you can use owner_model(x) to retrieve the model.)

I don’t really understand the differences in allocations though. I’d just pick the one that you like working with the best. Only worry about this if it is a computational bottleneck.

1 Like

Thanks for the insight! - especially since I didn’t know about owner_model(x) and that’s already coming in handy somewhere else! :grinning:

1 Like