Disclaimer: Эта статья является очень вольным переводом и некоторые мометы достаточно сильно отличаются от оригинала
Бороздя просторы интернета вы наверняка уже успели услышать про Rust. После всех красноречивых отзывов и расхваливаний вы, конечно же, не смогли не потрогать это чудо. Первая программа выглядела не иначе как:
Скомпилировав получим соответствующий исполняемый файл:
632 килобайт для простого принта?! Rust позиционируется как системный язык, который имеет потенциал для замены C/C++, верно? Так почему бы не проверить аналогичную программу на ближайшем конкуренте?
Более безопасные и громоздкие iostream-ы C++ выдают не сильно иной результат:
Кажется, что необычный размер исполняемых файлов Rust интересует много кого, и вопрос этот совершенно не нов. Взять, к примеру, этот вопрос на stackoverflow, или множество других. Даже немного странно, что все еще не было статей или каких-либо заметок описывающих эту проблему.
Любой опытный программист конечно же воскликнет, что дебаг билд на то и дебаг, и нередко его размер значительно превышает релиз-версию. Rust в данном случае не исключение и достаточно гибко позволяет настраивать параметры сборки. Уровни оптимизации аналогичны gcc, задать его можно с помощью параметра -C opt-level=x, где вместо x число от 0-3, либо s для минимизации размера. Ну что же, посмотрим что из этого выйдет:
Что удивительно каких-либо значительных изменений нет. На самом деле это происходит из-за того, что оптимизация применяется лишь к пользовательскому коду, а не к уже скомпонованной среде исполнения Rust.
Rust по стандартному поведению к каждому исполняемому файлу линкует всю свою стандартную библиотеку. Так что мы можем избавиться и от этого, ведь глупый линковщик не понимает, что нам не очень нужно взаимодействие с сетью.
На самом деле есть хорошая причина для такого поведения. Как вы наверное знаете языки C и C++ компилируют каждый файл по отдельности. Rust же поступает немного иначе, где единицей компиляции выступает крейт (crate). Не трудно догадаться, что вызов функций из других файлов компилятор не сможет оптимизировать, так как он попросту работает с одним большим файлом.
Изначально в C/C++ компилятор производил оптимизацию независимо каждого файла. Со временем появилась технология оптимизации при линковке. Хоть это и стало занимать значительно больше времени, зато в результате получались исполняемые файлы куда лучше, чем раньше. Посмотрим как изменит положение дел эта функциональность в Rust:
Первое, чем наверное стоит воспользоваться — это небезызвестная утилита strings из набора GNU Binutils. Вывод ее достаточно большой (порядка 6 тыс. строк), так что приводить его полностью не имеет смысла. Вот самое интересное:
На основе этого результата можно сделать несколько выводов:
— К исполняемым файлам Rust статически линкуется вся стандартная библиотека.
— Rust использует jemalloc вместо системного аллокатора
— К файлам также статически линкуется библиотека libbacktrace, которая нужна для трассировки стека
Все это, как вы понимаете, для обычного println не очень то и нужно. Значит самое время от них всех избавиться!
Начнем с простого — убрать из исполняемого файла отладочные символы.
Очень неплохой результат, почти половину исходного размера занимают отладочные символы. Хотя в этом случае удобочитаемого вывода при ошибках, вроде panic! нам не получить:
Вытащить целиком libbacktrace из линковки без последствий не получится, он сильно связан со стандартной библиотекой. Но зато размотка для паники из libunwind нам не нужна, и мы можем ее выкинуть. Незначительные улучшения мы все таки получим:
Компилятор Rust стандартной сборки чаще всего использует jemalloc, вместо системного аллокатора. Изменить это поведение очень просто: нужно всего лишь вставить макро и импортировать нужный крейт аллокатора.
Завершающим штрихом в нашем шаманстве могло быть удаление из исполняемого файла всей стандартной библиотеки. В большинстве случаев это не нужно, да и к тому же в офф.книге (или в переводе) все шаги подробно описаны. Этим способом можно получить файл размером, сопоставимым с аналогом на Си.
Стоит также отметить, что размер стандартного набора библиотек постоянен и сами линковочные файлы(перечисленные в статье) не увеличиваются в зависимости от вашего кода, а значит вам скорее всего не придется беспокоится о размерах. На крайний случай вы всегда можете использовать упаковщики кода вроде upx
Большое спасибо русскоязычному комьюнити Rust за помощь с переводом
Бороздя просторы интернета вы наверняка уже успели услышать про Rust. После всех красноречивых отзывов и расхваливаний вы, конечно же, не смогли не потрогать это чудо. Первая программа выглядела не иначе как:
fn main() {
println!("Hello, world!");
}
Скомпилировав получим соответствующий исполняемый файл:
$ rustc hello.rs
$ du -h hello
632K hello
632 килобайт для простого принта?! Rust позиционируется как системный язык, который имеет потенциал для замены C/C++, верно? Так почему бы не проверить аналогичную программу на ближайшем конкуренте?
$ cat hello.c
#include <stdio.h>
int main() {
printf("Hello, World!\n");
}
$ gcc hello.c -ohello
$ du -h hello
6.7K hello
Более безопасные и громоздкие iostream-ы C++ выдают не сильно иной результат:
$ cat hello.cpp
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
}
$ g++ hello.cpp -ohello
$ du -h 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
$ du -h helloworld
630K helloworld
Что удивительно каких-либо значительных изменений нет. На самом деле это происходит из-за того, что оптимизация применяется лишь к пользовательскому коду, а не к уже скомпонованной среде исполнения Rust.
Оптимизация линковки (LTO)
Rust по стандартному поведению к каждому исполняемому файлу линкует всю свою стандартную библиотеку. Так что мы можем избавиться и от этого, ведь глупый линковщик не понимает, что нам не очень нужно взаимодействие с сетью.
На самом деле есть хорошая причина для такого поведения. Как вы наверное знаете языки C и C++ компилируют каждый файл по отдельности. Rust же поступает немного иначе, где единицей компиляции выступает крейт (crate). Не трудно догадаться, что вызов функций из других файлов компилятор не сможет оптимизировать, так как он попросту работает с одним большим файлом.
Изначально в C/C++ компилятор производил оптимизацию независимо каждого файла. Со временем появилась технология оптимизации при линковке. Хоть это и стало занимать значительно больше времени, зато в результате получались исполняемые файлы куда лучше, чем раньше. Посмотрим как изменит положение дел эта функциональность в Rust:
$ rustc helloworld.rs -C opt-level=s -C lto
$ du -h 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
# du -h 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
$ du -h helloworld
592K helloworld
Убираем jemalloc
Компилятор Rust стандартной сборки чаще всего использует jemalloc, вместо системного аллокатора. Изменить это поведение очень просто: нужно всего лишь вставить макро и импортировать нужный крейт аллокатора.
#![feature(alloc_system)]
extern crate alloc_system;
fn main() {
println!("Hello, world!");
}
$ rustc helloworld.rs && du -h helloworld
235K helloworld
$ strip helloworld && du -h helloworld
133K helloworld
Небольшой вывод
Завершающим штрихом в нашем шаманстве могло быть удаление из исполняемого файла всей стандартной библиотеки. В большинстве случаев это не нужно, да и к тому же в офф.книге (или в переводе) все шаги подробно описаны. Этим способом можно получить файл размером, сопоставимым с аналогом на Си.
Стоит также отметить, что размер стандартного набора библиотек постоянен и сами линковочные файлы(перечисленные в статье) не увеличиваются в зависимости от вашего кода, а значит вам скорее всего не придется беспокоится о размерах. На крайний случай вы всегда можете использовать упаковщики кода вроде upx
Большое спасибо русскоязычному комьюнити Rust за помощь с переводом