Документация Engee

Советы по производительности

К настоящему моменту вы уже должны были ознакомиться с другими руководствами по началу работы. Вы почти готовы к написанию собственных моделей, но прежде следует узнать ряд важных моментов.

julia> using JuMP


julia> import HiGHS

Ознакомьтесь с советами по производительности Julia

В первую очередь следует прочитать раздел Советы по производительности в руководстве по Julia. Главное правило — избегать глобальных переменных. Это особенно важно, если вы изучаете JuMP после другого языка, например MATLAB.

Проблема с задержкой перед получением первого решения

Подобно печально известной проблеме с задержкой перед построением первого графика, в JuMP есть проблема с задержкой перед получением первого решения. Эта задержка происходит из-за того, что при первом вызове кода JuMP в каждом сеансе среде Julia необходимо скомпилировать большой объем кода, связанного с задачей. Над этой проблемой ведется активная работа, но вы со своей стороны тоже можете кое-что предпринять.

Предложение 1. Не вызывайте JuMP из командной строки

В других языках вы, возможно, привыкли к такому рабочему процессу:

$ julia my_script.jl

Для JuMP это не подходит, так как каждый запуск скрипта сопряжен с задержкой вследствие компиляции. Вместо этого используйте один из рабочих процессов, предлагаемых в документации Julia.

Предложение 2. Отключайте мосты, если они не используются

В настоящее время большинство задержек вызывается механизмом мостового соединения в JuMP. Если вы используете только те ограничения, которые изначально поддерживаются решателем, то можете отключить мосты, передав аргумент add_bridges = false в 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

Предложение 3. Используйте PackageCompiler

В качестве примера задержки вследствие компиляции рассмотрим следующую линейную программу с двумя переменными и двумя ограничениями:

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

Сохранение задачи в model.jl и ее вызов из командной строки приводит к следующему:

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

Очевидно, что 16 секунд — это слишком большие накладные расходы при решении такой простейшей модели. Однако задержка вследствие компиляции не зависит от размера задачи, поэтому для более крупных моделей, решение которых занимает минуты или часы, 16 дополнительных секунд может быть приемлемой потерей.

На тот случай, когда задержка вследствие компиляции неприемлема, совместимость JuMP с пакетом PackageCompiler.jl позволяет легко создать пользовательский системный образ (двоичное расширение Julia, кэширующее скомпилированный код), что существенно сокращает задержку. Пользовательский образ для нашей задачи можно создать следующим образом.

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

При запуске Julia с пользовательским образом время выполнения составляет всего 0,7 секунды вместо 16:

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

Другие настройки производительности, такие как отключение мостов или использование прямого режима, могут еще более сократить это время.

create_sysimage необходимо выполнять только один раз, а затем можно использовать один и тот же системный образ, пусть и с небольшим ущербом для производительности, даже при изменении model.jl или запуске другого файла.

Построение выражений с помощью макросов

Для построения выражений используйте макросы JuMP (или add_to_expression!). Старайтесь не создавать выражения вне макросов.

Построение выражения вне макроса приводит к созданию промежуточных копий выражения. Например:

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

эквивалентно

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

Поскольку нас интересует только c, выражения a и b не нужны, а их построение замедляет программу.

Макросы JuMP переписывают выражения так, что они выполняются на месте без дополнительных копий. Благодаря выделению меньшего объема памяти они работают быстрее, особенно в случае с большими выражениями.

Вот пример.

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]
69056

Макрос @allocated измеряет количество байтов, выделенных во время вычисления выражения. Чем их меньше, тем лучше.

Если воспользоваться макросом @expression, памяти будет выделяться гораздо меньше:

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

Отключение строковых имен

По умолчанию JuMP создает имена типа String для переменных и ограничений и передает их решателю. Преимущество передачи имен заключается в более понятных сообщениях журнала решателя (например, «переменная x имеет недопустимые границы» вместо «переменная v1203 имеет недопустимые границы»), но для более крупных моделей накладные расходы на передачу имен могут выходить за рамки допустимого.

Чтобы отключить создание имен типа String, задайте аргумент set_string_name = false в макросах @variable и @constraint или вызовите set_string_names_on_creation для отключения всех имен для определенной модели:

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

Обратите внимание, что это не меняет способ хранения символьных имен и привязок:

julia> x
_[1]

julia> model[:x]
_[1]

julia> x === model[:x]
true

Однако найти переменную по строковому имени теперь не получится:

julia> variable_by_name(model, "x") === nothing
true

Дополнительные сведения о различиях между строковыми именами, символьными именами и привязками см. в разделе Строковые имена, символьные имена и привязки.