Why are Rust executables so huge?
Asked Answered
J

7

361

I find the approach and the way they define the language in the first two chapters of the documentation particularly interesting. So I decided to get my fingers wet and started out with "Hello, world!".

I did so on Windows 7 x64, btw.

fn main() {
    println!("Hello, world!");
}

Issuing cargo build and looking at the result in targets\debug I found the resulting .exe being 3MB. After some searching (documentation of cargo command line flags is hard to find...) I found the --release option and created the release build. To my surprise, the .exe size has only become smaller by an insignificant amount: 2.99MB instead of 3MB.

My expectation would have been that a systems programming language would produce something compact.

Can anyone elaborate on what Rust is compiling to, how it can be possible it produces such huge images from a 3-line program? Is it compiling to a virtual machine? Is there a strip command I missed (debug info inside the release build?)? Anything else which might allow to understand what is going on?

Jobi answered 12/3, 2015 at 11:7 Comment(13)
I think 3Mb contains not only Hello World, but also all the needed environment for the platform. The same thing can be seen with Qt. That does not mean if you write a 6-line program the size will become 6 Mb. It will stay at 3Mb and will grow very slowly after that.Izzy
@AndreiNikolaenko I am aware of that. But this hints that either they do not handle libraries as C does, adding only what is required to an image or that something else is going on.Jobi
@user2225104 See my answer, RUST handles libraries in the same (or similar) way as C does, but by default C does not compile static libraries into your program (at least, on C++).Coffey
Very related: Do DLLs built with Rust require libgcc.dll on run time?.Junna
the strip.exe tool from MinGW seems working on EXE that rust generated. But I heard that rustc is build on llvm, not sure if there is any compatible problem.Boito
Not a fan of mingw, cygwin etc. If I program on windows, I program on windows not on poor mans linux substitutes. If rust claims to be a systems programming language they should not have trouble to support various platforms.Jobi
@Jobi would you like to try parameter -C link-args=-s to reduce the size (you said "strip command", I tried and it works). and parameter -C link-args=-static-libgcc will remove the dependency to libgcc_xxxx.dll for x86-32 platform. I don't like too many DLL denpendency, too. C/C++ has relative simpler runtime so generated exe size is not that large even for static linking. In case of file size, MSVC seems to do a good job. (mingw-g++ generated megabytes of exe for static linking as well)Boito
I found that -C opt-level=3 -C lto helps to reduce the size significantly for static libraries.Aseity
See min-sized-rust for an overview all of all of the different techniques to minimize binary size of Rust applications.Exception
Is this outdated now? With rustc version 1.35.0 and no cli options I get an exe that is 137kb in size. Does it automatically compile dynamically linked now or did something else happen in the meantime?Arundel
Here's an example of a hello world binary with tiny size that works on linux: mainisusuallyafunction.blogspot.com/2015/01/…Sastruga
Standalone self contained exe for dot net console app is 66MBCrematory
@AriesConnolly Not that it's really relevant to the discussion but if you enable trimming you can get the .NET self-contained executable down to 13MB.Varia
C
268

Rust uses static linking to compile its programs, meaning that all libraries required by even the simplest Hello world! program will be compiled into your executable. This also includes the Rust runtime.

To force Rust to dynamically link programs, use the command-line arguments -C prefer-dynamic; this will result in a much smaller file size but will also require the Rust libraries (including its runtime) to be available to your program at runtime. This essentially means you will need to provide them if the computer does not have them, taking up more space than your original statically linked program takes up.

For portability I'd recommend you statically link the Rust libraries and runtime in the way you have been doing if you were to ever distribute your programs to others.

Coffey answered 12/3, 2015 at 11:18 Comment(18)
Compile as rustc -C prefer-dynamic [file name].rs.Coffey
@user2225104 Unsure about Cargo, but according to this bug report on GitHub, this isn't yet possible unfortunately.Coffey
But as soon as you have more than 2 rust executables on a system, dynamic linking will start saving you space…Susian
I don't think static linking explains the huge HELLO-WORLD. Shouldn't it only link in the parts of the libraries that are actually used, and HELLO-WORLD uses virtually nothing?Bearable
BitTickler cargo rustc [--debug or --release] -- -C prefer-dynamic Cunaxa
Is it possible to install the Rust runtime library without the Rust compiler? Like installing glibc without GCC?Chang
@FranklinYu It's not currently possible - and probably won't be anytime soon. The main limitation is that rust releases are not binary compatible, and rust does not have a stable ABI. With dynamic linking, you'd need to do synchronized updates of all libraries and binaries at the same time; and do this each 6 weeks when a new compiler is released.Haggard
@Haggard Thank you very much. I have been tracking this related RFC. It's really a pity since Rust also targets system programming.Chang
Yeah - I'm hoping in 2-5 years there will be confidence in the language's optimization and binary format, and a stabilized ABI will exist. Hoping is about all I can do though :/.Haggard
@daboross: I hope the ABI doesn't get stabilized. An unstable ABI is necessary to pull off optimizations such as the "niche" optimization, changing struct layout, etc...Fuge
@MattheiM We could always have a versioned ABI with a clear spec written if we don't go 'fully' stable, though. I imagine that would have at least 90% of the benefits.Haggard
is Rust's executable larger tha Golang's executables?Tbilisi
@Nulik: Yes, by default, but that's because Rust defaults to static builds (all dependencies, including runtime, included), while Go links its runtime dynamically. On my CentOS 7 system, Go's helloworld compiles to ~76K, but on top of standard stuff, it takes a runtime dynamic dependency on libgo.so, which is over 47M. The default Rust helloworld (as made with cargo new) doesn't have any unique dynamic dependencies, holding everything but basic C runtime stuff in a 1.6M executable; with tweaks (optimize for size, using LTO, aborting on panic), it drops to 0.6M.Plenteous
The -C prefer-dynamic option gets the release builds (with only optimize for size enabled; it wouldn't let me use LTO or abort on panic) down to 8.8K, albeit with a new 4.7M dynamic dependency. So apples-to-apples, Rust is smaller; it's a tenth the size dynamically linked, relying on a runtime that's a tenth the size as well.Plenteous
Downvoting because this is wrong: Static linking does not necessarily imply obscenely obese binaries. Needless symbols can be stripped. Visual C++ 2017 x64 for example yields 219 kiB for int main(){std::cout<<"hello world\n";} with /EHsc /O2 /MT. Or Mingw-w64 g++ 10.2 914 kiB, passing -s -O2 -static. And this is with C++ Iostreams which are rather bloaty themselves and need to carry around formatting state, exceptions, locales and what not. Replacing with std::printf I get 117 kiB (cl) or 41 kiB (g++). Rust could improve on those 41 kiB as Rust I/O does not have locales yet it's way worse.Sampson
Fwiw, on my machine the asker's Rust example produces 4092 kiB by default (x86_64-pc-windows-gnu). Adding lto = true to Cargo.toml makes for 1406 kiB. Adding panic = "abort", opt-level = "z", codegen-units = 1 makes for 1264 kiB. The nightly setting strip = "symbols" reduces that to 220 kiB. This number might be reasonable for C++ but in my opinion there's no reason for Rust to produce such needlessly large executables. Rust has much less implied overhead compared to C++ as far as I can tell. It could do much better if people actually cared.Sampson
@Sampson This answer was correct at the time of writing. If that's changed, please edit the answer or add your own.Coffey
@Coffey I stand by my opinion: Your answer implies that static linking always comes with a huge binary size toll but you fail to point out that this is merely an issue with Rust's current implementation and default settings. It sounds to me as if static linking is a general problem which is not true. This is why I find your answer misleading.Sampson
E
326

By default, the Rust compiler optimizes for execution speed, compilation speed, and ease of debugging (by including symbols, for example), rather than minimal binary size.

For an overview of all of the ways to reduce the size of a Rust binary, see my min-sized-rust GitHub repository.

The current high level steps to reduce binary size are:

  1. Use Rust 1.32.0 or newer (which doesn't include jemalloc by default)
  2. Add the following to Cargo.toml:
[profile.release]
opt-level = 'z'     # Optimize for size
lto = true          # Enable link-time optimization
codegen-units = 1   # Reduce number of codegen units to increase optimizations
panic = 'abort'     # Abort on panic
strip = true        # Strip symbols from binary*

* strip = true requires Rust 1.59+. On older Rust versions, run strip manually on the resulting binary.

  1. Build in release mode using cargo build --release

There is more that can be done using nightly Rust, but I'll leave that information in min-sized-rust as it changes over time due to the use of unstable features.

You can also use #![no_std] to remove Rust's libstd. See min-sized-rust for details.

Exception answered 23/2, 2019 at 13:24 Comment(3)
Wow, this shrunk my executable from 50MB to 6MB! Was not expecting such a big improvementRakish
Compile with RUSTFLAGS='-C strip=symbols' cargo build --release to strip binary with stable rustc flagSitus
Just a note, these settings will have performance impacts, so make sure that isn't an issue for your usecase.Octodecillion
C
268

Rust uses static linking to compile its programs, meaning that all libraries required by even the simplest Hello world! program will be compiled into your executable. This also includes the Rust runtime.

To force Rust to dynamically link programs, use the command-line arguments -C prefer-dynamic; this will result in a much smaller file size but will also require the Rust libraries (including its runtime) to be available to your program at runtime. This essentially means you will need to provide them if the computer does not have them, taking up more space than your original statically linked program takes up.

For portability I'd recommend you statically link the Rust libraries and runtime in the way you have been doing if you were to ever distribute your programs to others.

Coffey answered 12/3, 2015 at 11:18 Comment(18)
Compile as rustc -C prefer-dynamic [file name].rs.Coffey
@user2225104 Unsure about Cargo, but according to this bug report on GitHub, this isn't yet possible unfortunately.Coffey
But as soon as you have more than 2 rust executables on a system, dynamic linking will start saving you space…Susian
I don't think static linking explains the huge HELLO-WORLD. Shouldn't it only link in the parts of the libraries that are actually used, and HELLO-WORLD uses virtually nothing?Bearable
BitTickler cargo rustc [--debug or --release] -- -C prefer-dynamic Cunaxa
Is it possible to install the Rust runtime library without the Rust compiler? Like installing glibc without GCC?Chang
@FranklinYu It's not currently possible - and probably won't be anytime soon. The main limitation is that rust releases are not binary compatible, and rust does not have a stable ABI. With dynamic linking, you'd need to do synchronized updates of all libraries and binaries at the same time; and do this each 6 weeks when a new compiler is released.Haggard
@Haggard Thank you very much. I have been tracking this related RFC. It's really a pity since Rust also targets system programming.Chang
Yeah - I'm hoping in 2-5 years there will be confidence in the language's optimization and binary format, and a stabilized ABI will exist. Hoping is about all I can do though :/.Haggard
@daboross: I hope the ABI doesn't get stabilized. An unstable ABI is necessary to pull off optimizations such as the "niche" optimization, changing struct layout, etc...Fuge
@MattheiM We could always have a versioned ABI with a clear spec written if we don't go 'fully' stable, though. I imagine that would have at least 90% of the benefits.Haggard
is Rust's executable larger tha Golang's executables?Tbilisi
@Nulik: Yes, by default, but that's because Rust defaults to static builds (all dependencies, including runtime, included), while Go links its runtime dynamically. On my CentOS 7 system, Go's helloworld compiles to ~76K, but on top of standard stuff, it takes a runtime dynamic dependency on libgo.so, which is over 47M. The default Rust helloworld (as made with cargo new) doesn't have any unique dynamic dependencies, holding everything but basic C runtime stuff in a 1.6M executable; with tweaks (optimize for size, using LTO, aborting on panic), it drops to 0.6M.Plenteous
The -C prefer-dynamic option gets the release builds (with only optimize for size enabled; it wouldn't let me use LTO or abort on panic) down to 8.8K, albeit with a new 4.7M dynamic dependency. So apples-to-apples, Rust is smaller; it's a tenth the size dynamically linked, relying on a runtime that's a tenth the size as well.Plenteous
Downvoting because this is wrong: Static linking does not necessarily imply obscenely obese binaries. Needless symbols can be stripped. Visual C++ 2017 x64 for example yields 219 kiB for int main(){std::cout<<"hello world\n";} with /EHsc /O2 /MT. Or Mingw-w64 g++ 10.2 914 kiB, passing -s -O2 -static. And this is with C++ Iostreams which are rather bloaty themselves and need to carry around formatting state, exceptions, locales and what not. Replacing with std::printf I get 117 kiB (cl) or 41 kiB (g++). Rust could improve on those 41 kiB as Rust I/O does not have locales yet it's way worse.Sampson
Fwiw, on my machine the asker's Rust example produces 4092 kiB by default (x86_64-pc-windows-gnu). Adding lto = true to Cargo.toml makes for 1406 kiB. Adding panic = "abort", opt-level = "z", codegen-units = 1 makes for 1264 kiB. The nightly setting strip = "symbols" reduces that to 220 kiB. This number might be reasonable for C++ but in my opinion there's no reason for Rust to produce such needlessly large executables. Rust has much less implied overhead compared to C++ as far as I can tell. It could do much better if people actually cared.Sampson
@Sampson This answer was correct at the time of writing. If that's changed, please edit the answer or add your own.Coffey
@Coffey I stand by my opinion: Your answer implies that static linking always comes with a huge binary size toll but you fail to point out that this is merely an issue with Rust's current implementation and default settings. It sounds to me as if static linking is a general problem which is not true. This is why I find your answer misleading.Sampson
S
111

I don't have any Windows systems to try on, but on Linux, a statically compiled Rust hello world is actually smaller than the equivalent C. If you are seeing a huge difference in size, it is probably because you are linking the Rust executable statically and the C one dynamically.

With dynamic linking, you need to take the size of all the dynamic libraries into account too, not just the executable.

So, if you want to compare apples to apples, you need to make sure either both are dynamic or both are static. Different compilers will have different defaults, so you can't just rely on the compiler defaults to produce the same result.

If you're interested, here are my results:

-rw-r--r-- 1 aij aij     63 Apr  5 14:26 printf.c
-rwxr-xr-x 1 aij aij   6696 Apr  5 14:27 printf.dyn
-rwxr-xr-x 1 aij aij 829344 Apr  5 14:27 printf.static
-rw-r--r-- 1 aij aij     59 Apr  5 14:26 puts.c
-rwxr-xr-x 1 aij aij   6696 Apr  5 14:27 puts.dyn
-rwxr-xr-x 1 aij aij 829344 Apr  5 14:27 puts.static
-rwxr-xr-x 1 aij aij   8712 Apr  5 14:28 rust.dyn
-rw-r--r-- 1 aij aij     46 Apr  5 14:09 rust.rs
-rwxr-xr-x 1 aij aij 661496 Apr  5 14:28 rust.static

These were compiled with gcc (Debian 4.9.2-10) 4.9.2 and rustc 1.0.0-nightly (d17d6e7f1 2015-04-02) (built 2015-04-03), both with default options and with -static for gcc and -C prefer-dynamic for rustc.

I had two versions of the C hello world because I thought using puts() might link in fewer compilation units.

If you want to try reproducing it on Windows, here are the sources I used:

printf.c:

#include <stdio.h>
int main() {
  printf("Hello, world!\n");
}

puts.c:

#include <stdio.h>
int main() {
  puts("Hello, world!");
}

rust.rs

fn main() {
    println!("Hello, world!");
}

Also, keep in mind that different amounts of debugging information, or different optimization levels would also make a difference. But I expect if you are seeing a huge difference it is due to static vs. dynamic linking.

Skuld answered 5/4, 2015 at 19:42 Comment(3)
gcc is smart enough to do exactly the printf -> puts substitution itself, that's why results are identical.Sunn
As of 2018 if you want a fair comparison do remember to "strip" the executables, as a hello world Rust executable on my system is a whopping 5.3MB but drops down to less than 10% of that when you remove all the debug symbols and such.Photometer
@MattiVirkkunen: Still the case in 2020; the natural size seems smaller (nowhere near 5.3M), but the ratio of symbols to code is still pretty extreme. The debug build, purely default options on Rust 1.34.0 on CentOS 7, stripped with strip -s, drops from 1.6M to 190K. The release build (defaults plus opt-level='s', lto = true, and panic = 'abort' to minimize size) drops from 623K to 158K.Plenteous
H
59

When compiling with Cargo, you can use dynamic linking:

cargo rustc --release -- -C prefer-dynamic

This will dramatically reduce the size of the binary, as it is now dynamically linked.

On Linux, at least, you can also strip the binary of symbols using the strip command:

strip target/release/<binary>

This will approximately halve the size of most binaries.

Heda answered 30/1, 2016 at 19:28 Comment(4)
Just some stats, default release version of hello world (linux x86_64). 3.5 M, with prefer-dynamic 8904 B, stripped 6392 B.Stope
should add a note saying that its bad for distributing.Slopwork
this result in an error on Windows. "The code execution cannot proceed because std-fd55ee3d3a94e250.dll was not found. Reinstalling the program may fix this problem."Carlycarlye
With the opt-level='z', lto='true', codegen-units='1', panic = 'abort', AND after stripping with strip -s <name_of_executable>, the size comes down to ~200 KB. Just wanted to say since this might be enough for most people size-hungry.Carse
P
5
#![no_main]
#![no_std]

#[link(name = "msvcrt", kind = "dylib")]
extern {
    fn puts(ptr: *const u8); // i8 or u8 doesn't matter in this case
}

#[no_mangle]
unsafe extern fn main() {
    puts("Hello, World!\0".as_ptr());
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

with next profile

[profile.release]
debug = false
strip = true
opt-level = 'z'
codegen-units = 1
lto = true
panic = 'abort'

gives 9 kb with -r, while C

#include <stdio.h>

main() {
    puts("Hello, World!");
}

gives 48 kb with GCC and -Os and 2 kb with TCC. Pretty impressive, isn't it?

Puffer answered 29/1, 2023 at 13:25 Comment(2)
Why is main here "unsafe"?Jobi
@bittickler, to be direct translation :) Actually, because puts is externed we need unsafe to call it. Making the function unsafe was easier than typing a block in this casePuffer
R
1

Install rust nightly - rustup toolchain install nightly, rustup default nightly

Now, make these changes in all the Cargo.toml files in your project.

Add cargo-features = ["strip"] before [package] at the top of the Cargo.toml

At the bottom, or between [dependencies] and [package] add,

[profile.release]
# strip = true  # Automatically strip symbols from the binary.
opt-level = "z"  # Optimize for size.
lto = true  # Enable link time optimization
codegen-units = 1  # Reduce parallel code generation units

Now build with RUSTFLAGS='-C link-arg=-s' cargo build --release

I found these links useful - https://collabora.com/news-and-blog/blog/2020/04/28/reducing-size-rust-gstreamer-plugin/ and https://github.com/johnthagen/min-sized-rust and https://arusahni.net/blog/2020/03/optimizing-rust-binary-size.html

Regardful answered 24/9, 2021 at 20:10 Comment(1)
Strip can now be done in stable rust with RUSTFLAGS='-C strip=symbols' cargo build --releaseSitus
P
-21

This is a feature, not a bug!

You can specify the library versions (in the project's associated Cargo.toml file) used in the program (even the implicit ones) to ensure library version compatibility. This, on the other hand, requires that the specific library be statically linked to the executable, generating large run-time images.

Hey, it's not 1978 any more - many people have more than 2 MB RAM in their computers :-)

Parley answered 15/2, 2018 at 5:3 Comment(3)
specify the library versions [...] requires that the specific library be statically linked — no, it doesn't. Plenty of code exists where exact versions of libraries are dynamically linked.Palmira
Well in 1978, we had a full OS or a Spreadsheet app in 8KB, and it worked. Yes we do much more things today, but it's useless to waste the ram, "light is right".Unfrequented
There's an important point here overlooked in most other posts. When using Cargo, if my code depends of v2 of a crate, and a difference dependency depends on v1 of that crate, rust will compile and link in both. And those crates are indeed both static linked. It makes dependency management less painful at the expense of binary size. It can help to review with cargo tree to find these duplicates and update crates to more recent versoins to avoid them.Inversion

© 2022 - 2024 — McMap. All rights reserved.