Avatar ·

Почему исполняемые файлы Rust такие огромные и как это исправить?

Всем привет! Я новичок в Rust и заметил странную вещь: даже самая простая программа занимает мегабайты на диске. Написал "Hello, World!" — и получил 3 МБ! В C/C++ такой же код весит килобайты. Что я делаю не так? Вот мой код: ```rust fn main() { println!("Hello, world!"); } ``` Собираю через `cargo build`. Файл target/debug/hello.exe — 3.2 МБ. Даже после `cargo build --release` размер уменьшился только до 2.8 МБ. Это нормально? Говорят, что Rust компилирует статически и включает рантайм, но неужели так много? Может, я пропустил какой-то флаг оптимизации? Или это особенность Rust? В документации написано, что Rust — системный язык, но такой размер для "Hello, World!" выглядит странно. Подскажите, как уменьшить размер бинарника? Заранее спасибо! P.S. Использую Windows 10, Rust 1.75.0. <<>>

Почему бинарники 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 --release2.8 МБ
+ lto = true + strip = true350 КБ
+ #![no_std] + wee_alloc8 КБ
+ 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.