Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Оптимизации: компромисс между скоростью и размером

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

Без оптимизаций

Это вариант по умолчанию. Когда вы вызываете cargo build, вы используете профиль разработки (dev). Этот профиль оптимизирован для отладки: он включает отладочную информацию и не включает никаких оптимизаций, то есть использует -C opt-level = 0.

По крайней мере для bare-metal разработки отладочная информация не имеет «стоимости» в том смысле, что она не занимает места во флеш-памяти / ПЗУ, поэтому мы фактически рекомендуем включить отладочную информацию в профиле release — по умолчанию она отключена. Это позволит вам использовать точки останова при отладке сборок release.

[profile.release]
# символы полезны, и они не увеличивают размер во флеш-памяти
debug = true

Отсутствие оптимизаций хорошо для отладки, потому что пошаговое выполнение кода ощущается так, будто вы выполняете программу оператор за оператором, и к тому же вы можете в GDB вывести значения стековых переменных и аргументов функций. Когда код оптимизирован, попытка вывести переменные приводит к тому, что печатается $0 = <value optimized out>.

Самый большой недостаток профиля dev в том, что итоговый бинарный файл будет огромным и медленным. Размер обычно создаёт больше проблем, потому что неоптимизированные бинарные файлы могут занимать десятки КиБ флеш-памяти, которой на вашем целевом устройстве может не хватить: в результате неоптимизированный бинарный файл просто не помещается в устройство!

Можно ли получить меньшие бинарные файлы, удобные для отладки? Да, есть один приём.

Оптимизация зависимостей

У Cargo есть возможность profile-overrides, которая позволяет переопределять уровень оптимизации зависимостей. С её помощью можно оптимизировать все зависимости по размеру, оставляя при этом верхний крейт неоптимизированным и удобным для отладки.

Имейте в виду, что обобщённый (generic) код иногда может оптимизироваться вместе с крейтом, в котором он создаётся, а не с крейтом, в котором он определён. Если вы создаёте экземпляр обобщённой структуры в своём приложении и обнаруживаете, что она подтягивает код большого объёма, возможно, увеличение уровня оптимизации соответствующих зависимостей не даст эффекта.

Вот пример:

# Cargo.toml
[package]
name = "app"
# ..

[profile.dev.package."*"] # +
opt-level = "z" # +

Без переопределения:

$ cargo size --bin app -- -A
app  :
section               size        addr
.vector_table         1024   0x8000000
.text                 9060   0x8000400
.rodata               1708   0x8002780
.data                    0  0x20000000
.bss                     4  0x20000000

С переопределением:

$ cargo size --bin app -- -A
app  :
section               size        addr
.vector_table         1024   0x8000000
.text                 3490   0x8000400
.rodata               1100   0x80011c0
.data                    0  0x20000000
.bss                     4  0x20000000

Это сокращение использования флеш-памяти примерно на 6 КиБ, причём верхний крейт остаётся удобным для отладки. Если вы зайдёте внутрь зависимости, то снова увидите сообщения <value optimized out>, но обычно вам нужно отлаживать верхний крейт, а не зависимости. А если вам действительно нужно отладить зависимость, вы можете воспользоваться profile-overrides, чтобы исключить конкретную зависимость из оптимизации. Пример ниже:

# ..

# не оптимизируем крейт `cortex-m-rt`
[profile.dev.package.cortex-m-rt] # +
opt-level = 0 # +

# но оптимизируем все остальные зависимости
[profile.dev.package."*"]
codegen-units = 1 # лучшие оптимизации
opt-level = "z"

Теперь верхний крейт и cortex-m-rt удобны для отладки!

Оптимизация по скорости

По состоянию на 2018-09-18 rustc поддерживает три уровня оптимизации по скорости: opt-level = 1, 2 и 3. Когда вы запускаете cargo build --release, вы используете профиль release, который по умолчанию использует opt-level = 3.

И opt-level = 2, и 3 оптимизируют по скорости за счёт размера бинарного файла, но уровень 3 выполняет больше векторизации и инлайнинга, чем уровень 2. В частности, вы увидите, что при opt-level, равном 2 или больше, LLVM разворачивает циклы. Разворачивание циклов имеет довольно высокую цену во флеш-памяти / ПЗУ (например, для цикла обнуления массива оно увеличивает код с 26 до 194 байтов), но при подходящих условиях (например, когда число итераций достаточно велико) может вдвое сократить время выполнения.

На данный момент нет способа отключить разворачивание циклов при opt-level = 2 и 3, поэтому, если вы не можете позволить себе его стоимость, оптимизируйте программу по размеру.

Оптимизация по размеру

По состоянию на 2018-09-18 rustc поддерживает два уровня оптимизации по размеру: opt-level = "s" и "z". Эти названия унаследованы от clang / LLVM и не очень описательны, но "z" задумано так, чтобы создавать ещё меньшие бинарные файлы, чем "s".

Если вы хотите, чтобы ваши бинарные файлы release были оптимизированы по размеру, измените настройку profile.release.opt-level в Cargo.toml, как показано ниже.

[profile.release]
# или "z"
opt-level = "s"

Эти два уровня оптимизации значительно снижают порог инлайнинга LLVM — метрику, которая используется, чтобы решить, инлайнить функцию или нет. Один из принципов Rust — абстракции с нулевой стоимостью; такие абстракции часто используют множество newtype-типов и маленьких функций для поддержания инвариантов (например, функций, которые заимствуют внутреннее значение, как deref, as_ref), поэтому низкий порог инлайнинга может помешать LLVM заметить возможности оптимизации (например, устранить мёртвые ветки или встроить вызовы замыканий).

При оптимизации по размеру стоит попробовать увеличить порог инлайнинга и посмотреть, влияет ли это на размер бинарного файла. Рекомендуемый способ изменить порог инлайнинга — добавить флаг -C inline-threshold к остальным rustflags в .cargo/config.toml.

# .cargo/config.toml
# предполагается, что вы используете шаблон cortex-m-quickstart
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
  # ..
  "-C", "inline-threshold=123", # +
]

Какое значение использовать? По состоянию на 1.29.0 таковы пороги инлайнинга, которые используют разные уровни оптимизации:

  • opt-level = 3 использует 275
  • opt-level = 2 использует 225
  • opt-level = "s" использует 75
  • opt-level = "z" использует 25

При оптимизации по размеру стоит попробовать значения 225 и 275.