Почему Rust-бинарники такие огромные? Лучшие практики 2025

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Призрак гигантских бинарников

Вы написали программу на Rust. Всего 200 строк кода, простой CLI-утилита для парсинга логов. Вы запускаете cargo build --release и с замиранием сердца смотрите на размер файла в папке target/release. 5… 10… 20 мегабайт? Для такого простого приложения? Знакомая боль, не правда ли?

Вопрос «Why are Rust executables so huge?» на Stack Overflow собрал тысячи просмотров и сотни комментариев. Действительно, сравните: аналогичная программа на C или Go часто весит 1–2 МБ, а на Rust — все 15. В чем подвох? Rust — это системный язык, который должен быть эффективным, так откуда берется лишний вес?

В этой статье мы честно разберем анатомию большого бинарника Rust, развенчаем популярные мифы и, главное, покажем проверенные способы сократить размер вашего приложения в 2–5 раз без потери производительности. Все примеры актуальны для Rust 2024–2025 годов.

Почему Rust-программы такие большие? Анатомия бинарника

Прежде чем паниковать, давайте заглянем внутрь нашего main.exe. Что именно занимает место?

1. Статическая линковка стандартной библиотеки

В отличие от Go, который линкует рантайм статически, или C, который часто использует общие библиотеки (DLL/so), Rust по умолчанию статически линкует std. Это значит, что вся стандартная библиотека, включая аллокатор, трейты, коллекции и прочее, вшивается прямо в ваш бинарник. Плюс — переносимость: не нужно таскать DLL. Минус — размер.

Даже простейший «Hello, World!» на Rust весит около 300–400 КБ (в release). Много? На самом деле, это уже оптимизировано: ранние версии Rust выдавали 1–2 МБ для того же кода.

2. Мономорфизация дженериков

Rust — язык с мономорфизацией дженериков. Это означает, что для каждого конкретного типа, с которым вызывается обобщенная функция, компилятор генерирует отдельную копию. Vec<u32> и Vec<String> — это два разных набора инструкций. Это дает максимальную производительность, но увеличивает размер.

3. Debug-символы и паника

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

4. Аллокатор по умолчанию

Rust использует jemalloc как аллокатор по умолчанию? Нет, начиная с Rust 1.32, аллокатор по умолчанию — системный (glibc на Linux, malloc на Windows). Однако, если вы используете jemalloc через крейт, его код тоже статически линкуется и добавляет 1–3 МБ.

Мифы о размере Rust-бинарников

  • Миф: «Rust всегда создает гигантские бинарники, это его проклятие». Реальность: Современный Rust (1.80+) с правильными флагами может создавать бинарники, сравнимые по размеру с C.
  • Миф: «Нужно использовать #![no_std] для маленьких бинарников». Реальность: Для embedded — да, но для обычных приложений можно получить отличный результат без отказа от стандартной библиотеки.
  • Миф: «Большой бинарник = плохая производительность». Реальность: Размер и скорость — разные вещи. Большой бинарник может быть очень быстрым.

Лучшие практики 2025: Как уменьшить Rust-бинарник

Теперь перейдем к практической части. Ниже — проверенные техники, которые работают в 2025 году. Применяйте их в зависимости от ваших целей: максимальная оптимизация для продакшена или минимальный размер для embedded.

Способ 1: Правильный профиль в Cargo.toml

Самый простой и эффективный шаг — настроить профиль релиза. Добавьте в Cargo.toml:

[profile.release]
opt-level = "z"   # Оптимизация по размеру (вместо "3" для скорости)
lto = "fat"       # Полная Link Time Optimization
codegen-units = 1 # Один юнит генерации кода — больше оптимизаций, но медленнее компиляция
strip = true      # Удалить все debug-символы

Что это дает?

  • opt-level = "z" — компилятор жертвует скоростью ради размера. Разница может быть 10–30%.
  • lto = "fat" — Link Time Optimization анализирует всю программу целиком, удаляя мертвый код и инлайня функции через границы крейтов. Это может сократить размер на 20–40%.
  • codegen-units = 1 — отключает параллельную генерацию кода, но позволяет LTO работать эффективнее.
  • strip = true — удаляет все символы и таблицы отладки. Это самый безболезненный способ сбросить 2–5 МБ.

После этих настроек простой HTTP-клиент на reqwest может уменьшиться с 15 МБ до 3–4 МБ.

Способ 2: Использовать strip вручную

Если вы не хотите менять профиль (например, для отладки), вы можете удалить символы после сборки:

# Linux
strip target/release/myapp

# macOS (используйте llvm-strip)
llvm-strip target/release/myapp

# Windows (используйте objcopy из MSVC)
objcopy --strip-all target/release/myapp.exe

Это уменьшит бинарник на 30–50% без изменения логики работы.

Способ 3: Отказ от ненужных зависимостей

Каждый крейт, который вы добавляете в Cargo.toml, потенциально увеличивает размер бинарника. Проверьте дерево зависимостей:

cargo tree

Ищите дублирующиеся или тяжелые крейты. Например, замена serde с деривацией на ручной парсинг может сэкономить 1–2 МБ. Используйте cargo-bloat для анализа того, что занимает место:

cargo install cargo-bloat
cargo bloat --release

Вы увидите таблицу: какие функции и крейты весят больше всего. Часто оказывается, что 80% размера занимает всего 2–3 крейта (например, regex или tokio).

Способ 4

Блоги

Рекомендаций по книгам