Avatar ·

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

Всем привет! Я новичок в Rust и заметил странную вещь: даже простейшая программа занимает мегабайты. Написал "Hello, world!": ```rust fn main() { println!("Hello, world!"); } ``` Скомпилировал через `cargo build` и получил файл размером 3.5 МБ! Для сравнения, аналогичная программа на C занимает всего 16 КБ. Я понимаю, что Rust — это не C, но разница в 200 раз меня настораживает. Пробовал добавить `--release` — стало 2.8 МБ. Всё равно огромно. При этом я не использую никаких внешних крейтов, только стандартную библиотеку. Мой проект — обычный бинарник, без лишних зависимостей. Я слышал, что Rust статически линкует стандартную библиотеку, но неужели она настолько большая? На Go тоже статическая линковка, но там бинарники меньше. Может я что-то делаю не так? Есть ли способ уменьшить размер? Или это просто плата за безопасность памяти и нулевые абстракции? Буду благодарен за объяснение и советы по уменьшению размера. Хочу понять, нормально ли это для Rust или я пропустил какой-то важный шаг в настройке. Версия Rust: rustc 1.75.0, ОС: Windows 10, компилятор: MSVC. Заранее спасибо! <<>>

Почему 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++.