Дешевые хитрости для высокопроизводительного Rust

Перевод | Автор оригинала: Pascal Hertleif

Итак, вы пишете Rust, но этого недостаточно? Даже если вы используете Cargo build --release? Вот несколько небольших вещей, которые вы можете сделать, чтобы увеличить скорость выполнения проекта на Rust - практически без изменения кода!

Помните, что следующие предложения не заменяют фактическое профилирование и оптимизацию! Я также думаю, что само собой разумеется, что единственный способ определить, помогает ли что-либо из этого, - это иметь тесты, которые показывают, как ваше приложение ведет себя при реальном использовании.

Если вы хотите прочитать об оптимизации производительности, которая занимает немного больше времени, но на самом деле касается улучшения вашего кода, взгляните на эту небольшую онлайн-книгу Николаса Нетеркота.

Настройка нашего профиля выпуска

Прежде всего, давайте включим еще несколько оптимизаций, когда мы выполняем сборку cargo - выпуск. Сделка довольно проста: мы включаем некоторые функции, которые делают сборку релизов еще медленнее, но в качестве награды получаем более тщательную оптимизацию.

Мы добавляем флаги, описанные ниже, в наш основной файл Cargo.toml, то есть в самый верхний файл манифеста, если вы используете рабочую область Cargo. Если у вас еще нет раздела под названием profile.release, добавьте его:

[profile.release]

Оптимизация времени компоновки

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

Rust может использовать несколько вариантов линкера, и мы хотим «оптимизировать для всех крэйтов», что называется «жирным». Чтобы установить это, добавьте в свой профиль флаг lto:

lto = "fat"

Блоки генерации кода

Далее аналогичная тема. Чтобы ускорить компиляцию, Rust пытается разбить ваши крэйти на небольшие части и компилировать как можно больше параллельно. Обратной стороной является то, что у компилятора меньше возможностей оптимизировать код для этих фрагментов. Итак, давайте посоветуем делать по одному фрагменту на крэйт:

codegen-units = 1

Установка конкретного целевого ЦП

По умолчанию Rust хочет создать двоичный файл, который работает на как можно большем количестве машин целевой архитектуры. Однако на самом деле у вас может быть довольно новый процессор с новыми классными функциями! Чтобы включить их, мы добавляем

-C target-cpu=native

в качестве «флага Rust», то есть переменной окружения RUSTFLAGS или целевого поля rustflags в вашем .cargo / config.

Прерывание

Теперь мы переходим к некоторым из наиболее небезопасных вариантов. Помните, как Rust по умолчанию использует раскрутку стека (на самых распространенных платформах)? Это стоит производительности! Давайте пропустим трассировку стека и возможность поймать панику для уменьшения размера кода и лучшего использования кеша:

panic = "abort"

Обратите внимание, что некоторые библиотеки могут зависеть от раскрутки и ужасно взорвутся, если вы включите это!

Использование другого распределителя

Многие программы на Rust выделяют память. И они не просто делают это сами, а фактически используют для этого (внешнюю) библиотеку: распределитель. Текущие двоичные файлы Rust по умолчанию используют системный распределитель по умолчанию, ранее они включали свой собственный в стандартную библиотеку. (Это изменение привело к уменьшению размера двоичных файлов и лучшей возможности отладки, что порадовало некоторых людей).

Однако иногда распределитель вашей системы - не лучший выбор. Не волнуйтесь, мы можем это изменить! Я предлагаю попробовать как jemalloc, так и mimalloc.

jemalloc

jemalloc - это распределитель, с которым ранее поставлялся Rust, и который компилятор Rust все еще использует сам. Его цель - уменьшить фрагментацию памяти и поддерживать высокий уровень параллелизма. Это также распределитель по умолчанию во FreeBSD. Если вам это интересно, давайте попробуем!

Во-первых, добавьте крэйт jemallocator в качестве зависимости:

[dependencies]
jemallocator = "0.3.2"

Затем в точке входа вашего приложения (main.rs) установите его как глобальный распределитель следующим образом:

#[global_allocator]
static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;

Обратите внимание, что jemalloc поддерживает не все платформы.

mimalloc

Другой интересный альтернативный распределитель - это mimalloc. Он был разработан Microsoft, имеет довольно небольшой размер и несколько новаторских идей для бесплатных списков.

Он также имеет настраиваемые функции безопасности (посмотрите его Cargo.toml). Это означает, что мы можем отключить их от большей производительности! Добавьте крэйт mimalloc как зависимость, например:

[dependencies]
mimalloc = { version = "0.1.17", default-features = false }

и, как и выше, добавьте это в свой файл точки входа:

#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

Профильная оптимизация

Это отличная функция LLVM, но я никогда ею не пользовался. Пожалуйста, прочтите документацию.

Актуальное профилирование и оптимизация вашего кода

Теперь именно здесь вам нужно скорректировать свой код и исправить все эти вызовы clone(). К сожалению, это тема для другого поста! (Пока вы ждете еще год, пока я напишу это, вы можете прочитать о коровах!)

Изменить: люди продолжают спрашивать эти актуальные советы о том, как оптимизировать код Rust. И, к счастью, я их обманул, у них есть хороший материал, на который я могу ссылаться:

Спасибо за чтение.