Большие бинари в моем Rust?

в 10:26, , рубрики: 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.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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js