Введение: Призрак гигантских бинарников
Вы написали программу на 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).