Оптимизации: компромисс между скоростью и размером
Все хотят, чтобы их программа была одновременно очень быстрой и очень маленькой, но обычно невозможно получить обе эти характеристики. В этом разделе рассказывается о разных уровнях оптимизации, которые предоставляет 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использует 275opt-level = 2использует 225opt-level = "s"использует 75opt-level = "z"использует 25
При оптимизации по размеру стоит попробовать значения 225 и 275.