Великі бинари в моєму Rust?

Disclaimer: Ця стаття досить є дуже вільним перекладом і деякі мометы досить сильно відрізняються від оригіналу

Борознячи простори інтернету ви напевно вже встигли почути про Rust. Після всіх красномовних відгуків і расхваливаний ви, звичайно ж, не змогли не помацати це диво. Перша програма виглядала не інакше як:
fn main() {
println!("Hello, world!");
}


Скомпілювавши отримаємо відповідний бинарь:
$ rustc hello.rs
$ ls -lh hello # зайвий висновок тут і далі вказано
632K hello

632 кілобайт для простого принта?! Rust позиціонується як системний мову, який має потенціал для заміни C/C++, вірно? Так чому б не перевірити аналогічну програму на найближчому конкуренту?
$ cat hello.c
#include < stdio.h>
int main() {
printf("Hello, World!\n");
}
$ gcc hello.c -ohello
$ ls -sh hello
6.7 K hello


Більш безпечні і громіздкі iostream-и C++ видають не сильно інший результат:
$ cat hello.cpp
#include < iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
}
$ g++ hello.cpp -ohello
$ ls -sh hello
8.3 K hello


Прапори -O3/-Os практично не змінюють кінцевого розміру


Так що не так з Rust?
Здається незвичайний розмір виконуваних файлів Rust цікавить багато кого і питання це зовсім не новий. Взяти, приміром, питання на stackoverflow, або безліч інші. Навіть трохи дивно, що все ще не було статей або будь-яких нотаток описують цю проблему.
Всі приклади були перетестированы на Rust 1.11.0-nightly (1ab87b65a 2016-07-02) на Linux 4.4.14 x86_64 без використання cargo та stable-гілки на відміну від оригінальної статті.


Рівень оптимізації
Будь-який досвідчений програміст звичайно ж скаже про те, що дебаг білд на те і дебаг, і нерідко його розмір значно перевищує реліз-версію. Rust в даному випадку не виняток і досить гнучко дозволяє налаштовувати параметри збірки. Рівні оптимізації аналогічні gcc, визначити його можна за допомогою параметра -C opt-level=x, де замість x число від 0-3, або s для мінімізації розміру. Ну що ж, подивимося що з цього вийде:
$ rustc helloworld.rs -C opt-level=s
$ ls -sh helloworld 
630K helloworld


Що дивно яких-небудь значних змін немає. Насправді це відбувається з-за того, що оптимізація застосовується лише до користувача кодом, а не до вже скомпонованої середовищі виконання Rust.

Оптимізація лінкування (LTO)
Rust за стандартним поведінки до кожного виконуваного файлу линкует всю свою стандартну бібліотеку. Так що ми можемо позбутися й від цього, адже дурний програма компонування не розуміє, що нам не дуже потрібно взаємодія з мережею.
Насправді є гарна причина для такої поведінки. Як ви напевно знаєте мови C і C++ компілює кожен файл окремо. Rust ж надходить трохи інакше, де одиницею компіляції виступає крейт (crate). Не важко здогадатися, що компілятор в даному випадку не зможе оптимізувати виклик функцій з інших файлів, так як він просто працює з одним великим.
Спочатку в C/C++ компілятор виробляв оптимізацію незалежно кожного файлу. З часом з'явилася технологія оптимізації при лінкування. Хоч це і стало займати значно більше часу, зате в результаті виходили виконувані файли куди краще, ніж раніше. Подивимося як змінить положення справ ця функціональність у Rust:
$ rustc helloworld.rs -C opt-level=s -C lto
$ Rust ls -sh helloworld
604K helloworld


Так що ж всередині?
Перше, чим напевно варто скористатися — це відома утиліта strings з набору GNU Binutils. Висновок її досить великий (близько 6 тис. рядків), так що наводити його повністю не має сенсу. Ось найцікавіше:
$ strings helloworld
capacity overflow
attempted to calculate the remainder with a divisor of zero
<jemalloc>: Error in atexit()
<jemalloc>: Error in pthread_atfork()
DW_AT_member
DW_AT_explicit
_ZN4core3fmt5Write9write_fmt17ha0cd161a5f40c4ade # core::fmt::Write::write_fmt::ha0cd161a5f40c4ad
_ZN4core6result13unwrap_failed17h072f7cd97aa67a9ce # core::result::unwrap_failed::h072f7cd97aa67a9c


На основі цього результату можна зробити кілька висновків:
— До виконуваних файлів Rust статично лінкуются вся стандартна бібліотека.
— Rust використовує jemalloc замість системного аллокатора
— До файлів також статично лінкуются бібліотека libbacktrace, яка потрібна для трасування стека
Все це, як ви розумієте, для звичайного println не дуже то й треба. Значить, саме час від них позбутися!

Налагоджувальні символи та libbacktrace
Почнемо з простого — прибрати з виконуваного файлу символи налагодження.
$ strip hello
# ls -lh hello
356K helloworld


Дуже непоганий результат, майже половину вихідного розміру займають символи налагодження. Хоча в цьому випадку зручного для читання виводу при помилках, начебто panic! нам не отримати:
$ cat helloworld.rs 
fn main() {
panic!("Hello, world!");
}
$ rustc helloworld.rs && RUST_BACKTRACE=1 ./helloworld 
thread 'main' panicked at 'Hello, world!', helloworld.rs:2
stack backtrace:
1: 0x556536e40e7f - std::sys::backtrace::tracing::imp::write::h6528da8103c51ab9
2: 0x556536e4327b - std::panicking::default_hook::_$u7b$$u7b$closure$u7d$$u7d$::hbe741a5cc3c49508
3: 0x556536e42eff - std::panicking::default_hook::he0146e6a74621cb4
4: 0x556536e3d73e - std::panicking::rust_panic_with_hook::h983af77c1a2e581b
5: 0x556536e3c433 - std::panicking::begin_panic::h0bf39f6d43ab9349
6: 0x556536e3c3a9 - helloworld::main::h6d97ffaba163087d
7: 0x556536e42b38 - std::panicking::try::call::h852b0d5f2eec25e4
8: 0x556536e4aadb - __rust_try
9: 0x556536e4aa7e - __rust_maybe_catch_panic
10: 0x556536e425de - std::rt::lang_start::hfe4efe1fc39e4a30
11: 0x556536e3c599 - main
12: 0x7f490342b740 - __libc_start_main
13: 0x556536e3c268 - _start
14: 0x0 - <unknown>
$ strip helloworld && RUST_BACKTRACE=1 ./helloworld
thread 'main' panicked at 'Hello, world!', helloworld.rs:2
stack backtrace:
1: 0x55ae4686ae7f <unknown>
...
11: 0x55ae46866599 <unknown>
12: 0x7f70a7cd9740 - __libc_start_main
13: 0x55ae46866268 <unknown>
14: 0x0 - <unknown>


Витягнути цілком libbacktrace з лінкування без наслідків не вийде, він сильно пов'язаний зі стандартною бібліотекою. Але зате розмотування для паніки libunwind нам не потрібна, і ми можемо її викинути. Незначні поліпшення ми все-таки отримаємо:
$ rustc helloworld.rs -C lto -C panic=abort -C opt-level=s
$ ls -lh helloworld
592K helloworld


Прибираємо jemalloc
Компілятор Rust стандартної складання найчастіше використовує jemalloc, замість системного аллокатора. Змінити цю поведінку дуже просто: потрібно всього лише вставити макро і імпортувати потрібний крейт аллокатора.
#![feature(alloc_system)]
extern crate alloc_system;

fn main() {
println!("Hello, world!");
}

$ rustc helloworld.rs && ls -lh helloworld
235K helloworld
$ strip helloworld && ls -lh helloworld 
133K helloworld


Невеликий висновок
Завершальним штрихом в нашому шаманстві могло бути видалення з виконуваного файлу всієї стандартної бібліотеки. В більшості випадків це не потрібно, та й до того ж в офф.книзі або перекладі) всі кроки докладно описані. Цим способом можна отримати файл розміром, порівнянним з аналогом на Сі.
Варто також зазначити, що розмір стандартного набору бібліотек бинаря константен і самі линковочные файли(перераховані в статті) не збільшуються в залежності від вашого коду, а значить вам швидше за все не доведеться турбуватися про розмірах. На крайній випадок ви завжди можете використовувати пакувальники коду начебто upx

Велике спасибі російськомовному ком'юніті Rust за допомогу з перекладом
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.