Big binaries in my rust?

Original author: Kang Seonghoon
  • Transfer
Disclaimer: This article is a very free translation and some points are quite different from the original.

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

Having compiled, we get the corresponding executable file:
$ rustc
$ 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 at your nearest competitor?
$ cat hello.c
int main() {
    printf("Hello, World!\n");
$ gcc hello.c -ohello
$ du -h hello
6.7K hello

More secure and bulky C ++ iostream yields not much different result:
$ cat hello.cpp
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 many people are interested in the unusual size of Rust executables, and this question is not new at all. Take, for example, this question on stackoverflow, or many others . It’s even a little strange that there were still no articles or any notes describing this problem.
All examples were tested on Rust 1.11.0-nightly (1ab87b65a 2016-07-02) on Linux 4.4.14 x86_64 without using a cargo and stable branch, unlike the original article.

Optimization level

Any experienced programmer, of course, will exclaim that the debug build is a debug and often its size is much larger than the release version. Rust in this case is no exception and it is flexible enough to allow you to configure the build parameters. The optimization levels are similar to gcc, it can be set using the -C opt-level = x parameter, where instead of x is a number from 0 - 3 , or s to minimize size. Well, let's see what comes of this:
$ rustc -C opt-level=s
$ du -h helloworld                 
630K helloworld

Surprisingly, there are no significant changes. This is actually due to the fact that the optimization applies only to user code, and not to the already built-in Rust runtime.

Link Optimization (LTO)

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

So what's inside?

The first thing you should probably use is the notorious strings utility from the GNU Binutils suite . Its conclusion is quite large (about 6 thousand lines), so bringing it completely does not make sense. Here is the fun part:
$ strings helloworld
capacity overflow
attempted to calculate the remainder with a divisor of zero
: Error in atexit()
: Error in pthread_atfork()
_ZN4core3fmt5Write9write_fmt17ha0cd161a5f40c4adE # или core::fmt::Write::write_fmt::ha0cd161a5f40c4ad
_ZN4core6result13unwrap_failed17h072f7cd97aa67a9cE # или core::result::unwrap_failed::h072f7cd97aa67a9c

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

Debugging Symbols and libbacktrace

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

A very good result, almost half of the original size is occupied by debugging symbols. Although in this case, readable output on errors like panic! we do not get:
$ cat 
fn main() {
    panic!("Hello, world!");
$ rustc && RUST_BACKTRACE=1 ./helloworld 
thread 'main' panicked at 'Hello, world!',
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 - 
$ strip helloworld && RUST_BACKTRACE=1 ./helloworld
thread 'main' panicked at 'Hello, world!',
stack backtrace:
   1:     0x55ae4686ae7f - 
  11:     0x55ae46866599 - 
  12:     0x7f70a7cd9740 - __libc_start_main
  13:     0x55ae46866268 - 
  14:                0x0 - 

It will not work without pulling out the entire libbacktrace from the link, it is strongly associated with the standard library. But we do not need unwinding for panic from libunwind , and we can throw it out. We’ll get minor improvements:
$ rustc -C lto -C panic=abort -C opt-level=s
$ du -h helloworld
592K helloworld

We remove jemalloc

The Rust compiler of the standard assembly most often uses jemalloc, instead of the system allocator. To change this behavior is very simple: you just need to insert a macro and import the desired allocator crate.
extern crate alloc_system;
fn main() {
    println!("Hello, world!");

$ rustc && du -h helloworld
235K helloworld
$ strip helloworld && du -h helloworld 
133K helloworld

Small conclusion

The final touch in our shamanism could be the removal of the entire standard library from the executable file. In most cases, this is not necessary, and besides, in the off-book (or translation ), all steps are described in detail. In this way, you can get a file with a size comparable to a C counterpart.
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 most likely will not have to worry about sizes. In extreme cases, you can always use code packers like upx.

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

Also popular now: