Почему исполняемые файлы Rust такие огромные и как это исправить?
Почему бинарники Rust большие?
Вы правы: даже простейшая программа на Rust может весить 2–5 МБ. Это не баг, а особенность компилятора. Основные причины:
- Статическая линковка — Rust по умолчанию включает всю стандартную библиотеку (std) в бинарник, включая аллокатор памяти (jemalloc или системный), обработку паники, форматирование строк и т.д.
- Мономорфизация дженериков — для каждого типа генерируется отдельная версия функции.
- Debug-символы — в debug-режиме они занимают до 70% размера.
- Стандартный аллокатор — jemalloc (на Linux/macOS) или системный аллокатор с большим количеством служебного кода.
Как уменьшить размер бинарника?
Вот проверенные методы, от простых к продвинутым:
1. Сборка в release-режиме с LTO
cargo build --release
# Добавьте в Cargo.toml:
[profile.release]
lto = true # Link-Time Optimization — агрессивная оптимизация
codegen-units = 1 # Уменьшает количество единиц компиляции (больше оптимизаций)
strip = true # Удаляет debug-символы (rustc 1.59+)
opt-level = "z" # Оптимизация под размер (вместо "3" — под скорость)
После этого "Hello, World!" уменьшится с 3 МБ до ~300–500 КБ.
2. Использование #![no_std]
Если ваша программа не требует стандартной библиотеки (нет ввода-вывода, коллекций), можно отказаться от std:
// src/main.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn _start() -> ! {
// Ваш код без std
loop {}
}
Такой бинарник может весить всего 2–10 КБ, но вы потеряете println!, Vec, String и другие удобства.
3. Минимизация зависимостей
Каждый крейт добавляет код. Используйте cargo-bloat для анализа:
cargo install cargo-bloat
cargo bloat --release
Он покажет, какие функции занимают больше всего места.
4. Сжатие UPX
После сборки можно сжать бинарник утилитой UPX (Ultimate Packer for eXecutables):
upx --best target/release/hello.exe
# Сжимает с 300 КБ до ~80 КБ
5. Удаление лишнего аллокатора
Замените стандартный аллокатор на более легкий (например, wee_alloc или dlmalloc):
// Cargo.toml
[dependencies]
wee_alloc = "0.4"
// src/main.rs
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
Сравнение размеров (пример)
| Конфигурация | Размер |
|---|---|
| cargo build (debug) | 3.2 МБ |
| cargo build --release | 2.8 МБ |
| + lto = true + strip = true | 350 КБ |
| + #![no_std] + wee_alloc | 8 КБ |
| + UPX сжатие | 2 КБ |
Для большинства проектов достаточно просто добавить lto = true и strip = true в профиль release. Размер в 300–500 КБ для "Hello, World!" — это нормально для Rust, учитывая всю встроенную функциональность (безопасность памяти, обработка ошибок, форматирование).
Альтернативный подход: использование минимального рантайма
Если вы пишете встраиваемое ПО или микросервис, где каждый килобайт на счету, можно полностью отказаться от стандартного рантайма Rust. Вместо #![no_std] используйте крейт cargo-generate с шаблоном для микроконтроллеров или минимального бинарника.
Пример для Linux x86_64:
// Cargo.toml
[dependencies]
libc = "0.2"
// src/main.rs
#![no_std]
#![no_main]
#[no_mangle]
pub extern "C" fn main() -> i32 {
0
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
Такой бинарник весит ~1.5 КБ после strip и UPX. Однако он не может использовать println!, Vec, String и другие возможности std. Для вывода придётся вызывать libc::write напрямую.
Ещё один вариант — использовать musl вместо glibc для статической линковки. Rust поддерживает musl через target-тройку x86_64-unknown-linux-musl. Это уменьшит размер на 200–400 КБ за счёт более лёгкой libc.
Установка:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
Помните: уменьшение размера почти всегда идёт в ущерб удобству разработки. Для 99% приложений достаточно просто включить LTO и strip в Cargo.toml.