📜 ⬆️ ⬇️

Big Binary in my Rust?

Disclaimer: This article is a very free translation, and some moments are quite different from the original.

Plying the Internet, you probably already heard about Rust. After all the eloquent reviews and praises you, of course, could not help but touch this miracle. The first program looked like:
fn main() { println!("Hello, world!"); } 


Compiling we get the corresponding executable file:
 $ rustc hello.rs $ du -h hello 632K hello 

632 kilobytes for a simple print? Rust is positioned as a system language that has the potential to replace C / C ++, right? So why not check out a similar program on a close competitor?
 $ cat hello.c #include <stdio.h> int main() { printf("Hello, World!\n"); } $ gcc hello.c -ohello $ du -h hello 6.7K hello 

')
Safer and cumbersome C ++ iostream s produce a slightly different result:
 $ cat hello.cpp #include <iostream> int main() { std::cout << "Hello, World!" << std::endl; } $ g++ hello.cpp -ohello $ du -h hello 8.3K hello 


Flags -O3 / -Os practically do not change the final size


So what's wrong with Rust?


It seems that the unusual size of Rust executables is of interest to many, and this question is completely new. Take, for example, this question on stackoverflow, or many others . It is even a bit strange that there are still no articles or any notes describing this problem.
All examples were re-tested on Rust 1.11.0-nightly (1ab87b65a 2016-07-02) on Linux 4.4.14 x86_64 without using cargo and stable-branch, unlike the original article.


Optimization level


Of course, any experienced programmer will exclaim that the debug build is debugging, and often its size significantly exceeds the release version. Rust in this case is not an exception and rather flexibly allows you to customize the build parameters. Optimization levels are similar to gcc, you can set it using the -C opt-level = x parameter, where instead of x is a number from 0 - 3 , or s to minimize the size. Well, let's see what comes of it:
 $ rustc helloworld.rs -C opt-level=s $ du -h helloworld 630K helloworld 


What is surprising is that there are no significant changes. In fact, this is due to the fact that the optimization is applied only to the user code, and not to the already assembled runtime environment Rust.

Link Optimization (LTO)


Rust on the standard behavior to each executable file links all its standard library. So we can get rid of this, because a stupid linker does not understand that we do not really need to interact with the network.
There is actually a good reason for this behavior. As you probably know, C and C ++ languages ​​compile each file separately. Rust does a little differently, where crate is the unit of compilation. It is not difficult to guess that the compiler will not be able to optimize the calling of functions from other files, since it simply works with one large file.
Initially, in C / C ++, the compiler optimized independently for each file. Over time, optimization technology appeared with linking. Although it began to take much more time, but as a result, executable files were obtained much better than before. Let's see how this functionality will change in Rust:
 $ rustc helloworld.rs -C opt-level=s -C lto $ du -h helloworld 604K helloworld 


So what's inside?


The first thing you should probably use is the well-known strings utility from the GNU Binutils suite. Its output is quite large (about 6 thousand lines), so it does not make sense to bring it completely. Here is the most interesting:
 $ 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 


Based on this result, several conclusions can be drawn:
- The entire standard library is statically linked to Rust executable files.
- Rust uses jemalloc instead of system allocator
- The files are also statically linked by the libbacktrace library, which is needed to trace the stack.
All this, as you understand, is not very necessary for ordinary println. So it's time to get rid of them all!

Debug symbols and libbacktrace


Let's start with a simple one - remove debug symbols from the executable file.
 $ strip hello # du -h hello 356K helloworld 


Very good result, debugging symbols occupy almost half of the original size. Although in this case readable output for errors like panic! we do not get:
 $ 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> 


Pulling the entire libbacktrace from the link without consequences will not work, it is strongly associated with the standard library. But then we don’t need to unwind from libunwind to panic, and we can throw it out. Minor improvements, we still get:
 $ rustc helloworld.rs -C lto -C panic=abort -C opt-level=s $ du -h helloworld 592K helloworld 


Remove jemalloc


The standard build Rust compiler often uses jemalloc, instead of the system allocator. Changing this behavior is very simple: you just need to insert a macro and import the required allocator crate.
 #![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 


Small conclusion


The final touch in our shamanism could be the removal from the executable file of the entire standard library. In most cases this is not necessary, and besides, in the off-book (or in translation ) all the steps are described in detail. This way you can get a file with a size comparable to the analogue in C.
It is also worth noting that the size of the standard set of libraries is constant and the link files themselves (listed in the article) do not increase depending on your code, which means you probably don’t have to worry about sizes. In extreme cases, you can always use code wrappers like upx

Many thanks to the Russian-speaking community Rust for help with the translation

Source: https://habr.com/ru/post/305246/


All Articles