Почему исполняемые файлы Rust такие огромные и как это исправить?
Почему Rust-бинарники большие: основные причины
Причина огромного размера "Hello, world!" на Rust — это не просто статическая линковка, а целый комплекс факторов. Давайте разберём их по порядку:
- Статическая линковка libstd: Rust всегда статически линкует стандартную библиотеку. Это даёт гарантию, что программа будет работать на любой системе, но добавляет ~1-2 МБ кода.
- Мономорфизация дженериков: Даже простой
println!использует много обобщённого кода, который компилятор разворачивает в конкретные версии для каждого типа. - Debug-символы: При сборке без флагов (debug-режим) Cargo включает отладочные символы. Они могут занимать до 70% размера бинарника.
- Codegen-юниты: По умолчанию Rust компилирует каждый крейт как один юнит, что ограничивает межмодульную оптимизацию.
- Бэкенд LLVM: Rust использует LLVM, который не всегда оптимально сжимает код по умолчанию.
Как это исправить: пошаговое руководство
Вот проверенный набор оптимизаций, которые уменьшат ваш бинарник с 3.5 МБ до ~300 КБ и ниже:
Шаг 1: Release-сборка и strip символов
Добавьте в Cargo.toml:
[profile.release]
opt-level = "z" # Оптимизация под размер
lto = true # Линковка с оптимизацией времени
codegen-units = 1 # Максимальная оптимизация
strip = true # Удаление символов (Rust 1.59+)
После этого cargo build --release даст бинарник ~500 КБ.
Шаг 2: Минимизация стандартной библиотеки
Если вам не нужен полный функционал std, используйте #![no_std] с крейтом libc:
// src/main.rs
#![no_std]
#![no_main]
#[panic_handler]
fn panic(_: &core::panic::PanickInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn main() -> i32 {
0
}
Такой бинарник будет весить ~2-5 КБ, но вы потеряете println!, Vec, String и другие удобства.
Шаг 3: Использование UPX
После сборки сожмите бинарник упаковщиком UPX:
upx --best target/release/my_program.exe
Это сожмёт его до 50-100 КБ (но замедлит запуск на ~10-20 мс).
Шаг 4: Отключение фич
Проверьте, какие фичи стандартной библиотеки вам реально нужны. Для минимального вывода:
// Cargo.toml
[dependencies]
libc = "0.2"
// main.rs
use libc::puts;
fn main() {
unsafe { puts("Hello\0".as_ptr() as *const i8); }
}
Почему это нормально?
Rust жертвует размером ради производительности и безопасности. Даже "огромный" бинарник на 2-3 МБ — это ничто для современных систем. Для сравнения:
- Python-скрипт требует ~30 МБ интерпретатора
- Node.js программа — ~50 МБ рантайма
- Rust-бинарник — самодостаточен и не требует ничего
Так что 3 МБ — это не "огромно", а вполне разумно. Если же вам критичен размер (например, для embedded), используйте #![no_std] и ручную линковку.
Альтернативный подход: настройка линкера и использование nightly
Если вы не хотите отказываться от стандартной библиотеки, попробуйте продвинутые техники оптимизации размера:
1. Замена линкера на LLD
LLD (LLVM Linker) создаёт более компактные бинарники, чем MSVC или GNU ld. Установите:
rustup component add llvm-tools-preview
И добавьте в .cargo/config.toml:
[target.x86_64-pc-windows-msvc]
linker = "lld-link.exe" # Для Windows
# Для Linux:
# [target.x86_64-unknown-linux-gnu]
# linker = "lld"
2. Использование nightly и LTO с ThinLTO
Переключитесь на nightly и включите ThinLTO:
rustup default nightly
# В Cargo.toml:
[profile.release]
lto = "thin"
ThinLTO даёт почти такой же выигрыш, как полный LTO, но компилируется быстрее.
3. Удаление ненужных секций
Добавьте флаги для удаления мёртвого кода:
# .cargo/config.toml
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "link-args=/OPT:REF,/OPT:ICF"]
# Для Linux:
# rustflags = ["-C", "link-args=-Wl,--gc-sections,-Wl,--icf=all"]
4. Использование min-sized-rust
Существует готовый рецепт min-sized-rust, который объединяет все эти техники. Просто скопируйте их конфигурацию.
Результат: с этими настройками "Hello, world!" можно сжать до ~150-200 КБ без потери функциональности std. Это уже вполне конкурентоспособно с Go или C++.
Ответов пока нет.
Войдите чтобы оставить ответ