Performance tips

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

By now you should have read the other "getting started" tutorials. You're almost ready to write your own models, but before you do so there are some important things to be aware of.

julia> using JuMP
julia> import HiGHS

Read the Julia performance tips

The first thing to do is read the Performance tips section of the Julia manual. The most important rule is to avoid global variables. This is particularly important if you're learning JuMP after using a language like MATLAB.

The "time-to-first-solve" issue

Similar to the infamous time-to-first-plot plotting problem, JuMP suffers from time-to-first-solve latency. This latency occurs because the first time you call JuMP code in each session, Julia needs to compile a lot of code specific to your problem. This issue is actively being worked on, but there are a few things you can do to improve things.

Suggestion 1: don't call JuMP from the command line

In other languages, you might be used to a workflow like:

$ julia my_script.jl

This doesn't work for JuMP, because we have to pay the compilation latency every time you run the script. Instead, use one of the suggested workflows from the Julia documentation.

Suggestion 2: disable bridges if none are being used

At present, the majority of the latency problems are caused by JuMP's bridging mechanism. If you only use constraints that are natively supported by the solver, you can disable bridges by passing add_bridges = false to Model.

julia> model = Model(HiGHS.Optimizer; add_bridges = false)A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none

Suggestion 3: use PackageCompiler

As an example of compilation latency, consider the following linear program with two variables and two constraints:

using JuMP, HiGHS
model = Model(HiGHS.Optimizer)
set_silent(model)
@variable(model, x >= 0)
@variable(model, 0 <= y <= 3)
@objective(model, Min, 12x + 20y)
@constraint(model, c1, 6x + 8y >= 100)
@constraint(model, c2, 7x + 12y >= 120)
optimize!(model)
open("model.log", "w") do io
    print(io, solution_summary(model; verbose = true))
    return
end

Saving the problem in model.jl and calling from the command line results in:

$ time julia model.jl
15.78s user 0.48s system 100% cpu 16.173 total

Clearly, 16 seconds is a large overhead to pay for solving this trivial model. However, the compilation latency is independent on the problem size, and so 16 seconds of additional overhead may be tolerable for larger models that take minutes or hours to solve.

In cases where the compilation latency is intolerable, JuMP is compatible with the PackageCompiler.jl package, which makes it easy to generate a custom sysimage (a binary extension to Julia that caches compiled code) that dramatically reduces the compilation latency. A custom image for our problem can be created as follows:

using PackageCompiler, Libdl
PackageCompiler.create_sysimage(
    ["JuMP", "HiGHS"],
    sysimage_path = "customimage." * Libdl.dlext,
    precompile_execution_file = "model.jl",
)

When Julia is run with the custom image, the run time is now 0.7 seconds instead of 16:

$ time julia --sysimage customimage model.jl
0.68s user 0.22s system 153% cpu 0.587 total

Other performance tweaks, such as disabling bridges or using direct mode can reduce this time further.

Note

create_sysimage only needs to be run once, and the same sysimage can be used–to a slight detriment of performance–even if we modify model.jl or run a different file.

Use macros to build expressions

Use JuMP's macros (or add_to_expression!) to build expressions. Avoid constructing expressions outside the macros.

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

x[1] + x[2] + x[3]

is equivalent to

a = x[1]
b = a + x[2]
c = b + x[3]

Since we only care about c, the a and b expressions are not needed and constructing them slows the program down.

JuMP's macros rewrite the expressions to operate in-place and avoid these extra copies. 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]

Here's what happens if we construct the expression outside the macro:

julia> @allocated x[1] + x[2] + x[3]1232
Info

The @allocated measures how many bytes were allocated during the evaluation of an expression. Fewer is better.

If we use the @expression macro, we get many fewer allocations:

julia> @allocated @expression(model, x[1] + x[2] + x[3])672

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.