Performance tips

This tutorial was generated using Literate.jl. Download the source as a .jl file.

Good performance in JuMP requires understanding a handful of patterns that differ from general Julia advice. This tutorial highlights the most important performance issues specific to JuMP, complementing the Julia manual's Performance tips.

Learning intentions:

  • Use JuMP's macros (for example, @expression, @constraint) to build expressions efficiently and avoid unnecessary intermediate allocations
  • Use add_to_expression! to accumulate terms in-place instead of via operator overloading
  • Disable string name creation with set_string_names_on_creation for large models, and pre-compute non-zero index sets to skip zero-coefficient terms

Required packages

This tutorial uses the following packages:

julia> using JuMP
julia> import HiGHS

Use macros to build expressions

Use JuMP's macros to build expressions.

Constructing an expression outside the macro results in intermediate copies of the expression. For example,

result = x[1] + x[2] + x[3]

is equivalent to

tmp = x[1] + x[2]
result = tmp + x[3]

Since we only care about result, the tmp expression is not needed, and constructing it slows the program down.

JuMP's macros rewrite the expressions to operate in-place and avoid temporary expressions. Because they allocate less memory, they are faster, particularly for large expressions.

Here's an example.

julia> model = Model()A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> @variable(model, x[1:3])3-element Vector{VariableRef}: x[1] x[2] x[3]
julia> @allocated x[1] + x[2] + x[3]1168
julia> @allocated @expression(model, x[1] + x[2] + x[3])784

Use add_to_expression! to build summations

If you don't want to use the expression macros, use add_to_expression! to build summations. For example, instead of:

julia> expr = zero(AffExpr)0
julia> for i in 1:3 expr += x[i] end
julia> exprx[1] + x[2] + x[3]

do

julia> expr = zero(AffExpr)0
julia> for i in 1:3 add_to_expression!(expr, x[i]) end
julia> exprx[1] + x[2] + x[3]

The former is equivalent to:

julia> expr0 = zero(AffExpr)0
julia> expr1 = expr0 + x[1]x[1]
julia> expr2 = expr1 + x[2]x[1] + x[2]
julia> expr = expr2 + x[3]x[1] + x[2] + x[3]

which allocates four unique AffExpr objects. The latter efficiently updates expr in-place so that only one AffExpr object is allocated.

The function add_to_expression! also supports terms like y += a * x where a is a constant. For example, instead of:

julia> expr = zero(AffExpr)0
julia> for i in 1:3 expr += i * x[i] end
julia> exprx[1] + 2 x[2] + 3 x[3]

do

julia> expr = zero(AffExpr)0
julia> for i in 1:3 add_to_expression!(expr, i, x[i]) end
julia> exprx[1] + 2 x[2] + 3 x[3]

Don't do this, because i * x[i] will allocate a new AffExpr in each iteration:

julia> expr = zero(AffExpr)0
julia> for i in 1:3 add_to_expression!(expr, i * x[i]) end
julia> exprx[1] + 2 x[2] + 3 x[3]

Disable string names

By default, JuMP creates String names for variables and constraints and passes these to the solver. The benefit of passing names is that it improves the readability of log messages from the solver (for example, "variable x has invalid bounds" instead of "variable v1203 has invalid bounds"), but for larger models the overhead of passing names can be non-trivial.

Disable the creation of String names by setting set_string_name = false in the @variable and @constraint macros, or by calling set_string_names_on_creation to disable all names for a particular model:

julia> model = Model();
julia> set_string_names_on_creation(model, false)
julia> @variable(model, x)_[1]
julia> @constraint(model, c, 2x <= 1)2 _[1] ≤ 1

Note that this doesn't change how symbolic names and bindings are stored:

julia> x_[1]
julia> model[:x]_[1]
julia> x === model[:x]true

But you can no longer look up the variable by the string name:

julia> variable_by_name(model, "x") === nothingtrue
Info

For more information on the difference between string names, symbolic names, and bindings, see String names, symbolic names, and bindings.

Sparsity

JuMP executes the code that you write without trying to be clever about multiplication by 0.0. For example, consider this model:

julia> d = 100_000100000
julia> a = zeros(d);
julia> a[3] = 1.01.0
julia> model = Model();
julia> @variable(model, x[1:d]);
julia> complicated_expression(x, i) = x[i]^2complicated_expression (generic function with 1 method)
julia> @expression(model, sum(a[i] * complicated_expression(x, i) for i in 1:d))x[3]²

Although the final expression consists of a single element, the sum is over 100,000 elements, all but one of which are then multiplied by 0.0. The @expression line is equivalent to:

julia> expr = zero(QuadExpr)0
julia> for i in 1:d tmp = complicated_expression(x, i) global expr = add_to_expression!(expr, a[i], tmp) end

Notice how we compute complicated_expression in every iteration, even though most results will be discarded. You can improve the performance of model construction by pre-computing the set of non-zero indices:

julia> indices = [i for i in 1:d if !iszero(a[i])]1-element Vector{Int64}:
 3
julia> @expression(model, sum(a[i] * complicated_expression(x, i) for i in indices))x[3]²