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