Monster binaries in Windows

I was pleased to find that the Rust install on Windows was pretty seamless, apart from the unsigned installer, but what interests me is how the generated binaries compare.

I compared a simple C hello world, using the Microsft toolset, against the equivalent Rust program.

My compile/link used nothing but defaults, the command line for rust, "rustc hello.rs", for C, "cl helloc.c"

On windows, a simple hello world seems a tad bloated at 3036103 bytes vs a native C hello world at 73216 bytes. A factor of 41, yikes!

The pain only starts there, as DLLs are not included in the exe itself. I dug a bit deeper with dumpbin. Let's look at the C imports... a bunch of functions from just one DLL, kernel32.exe, not too shabby, now for Rust., it needs kernel.dll too, but then it also demands the following...

ADVAPI32.dll
msvcrt.dll
SHELL32.dll
USERENV.dll
WS2_32.dll

Rust the langauge I like, Rust the implementation, less so.

  1. Your rust binary includes rust's standard library. Try messing around with no_std.
  2. Rust's standard library is larger than C's (more comparable to C++'s) so even if you statically linked C's standard library into a C program, the rust binary would still be larger.

To make a better comparison, try writing hello world in C++ (a more comparable language) and statically linking the standard library (in gcc, the relevant flag is -static-libstdc++). On my Linux machine, the following C++ program compiled with g++ -static-libstdc++ is 773K:

#include<iostream>
using namespace std;
int main() {
    cout<<"Hello world"<<endl;
    return 0;
}

and is linked against linux-vdso.so, libm.so, libgcc_s.so, libc.so, and ld-linux-x86-64.so.

The comparable rust program below is 635K:

fn main() {
    println!("hello world");
}

And links against linux-vdso.so, libdl.so, libpthread.so, librt.so, libgcc_s.so, libc.so, ld-linux-x86-64.so, and libm.so.

Yeah, Rust Windows support is a bit lacking. Plus the LLVM doesn't run effortlessly on Windows either, IIRC.

If I compile a trivial hello.rs using rustc hello.rs -C prefer-dynamic, I get a 49,450 byte executable that is linked against std-4e7c5e5c.dll, libgcc_s_dw2-1.dll, kernel32.dll and msvcrt.dll.

Looks fine to me.

Try with -C opt-level=3 -C lto -C link-args=-s. That gave me about 400KB binary, which is not optimal but much better than 3MB. The resulting binary only links to kernel32.dll and msvcrt.dll (I've verified that with depends.exe).

Did that, not with unix libraries, remember, Rust isn't up against gcc and friends. So, your program, no changes, compiled out of the box using CL defaults, 148,480 bytes, twice as big as my pure C code (is it really ure that Rust doesn't wan tto challenge C?)

But still, now the ratio is 20, more or less, much better, but still an order of magnitude.

It will take a bit longer but I will try some of the other suggestions.

I still REALLY want to use Rust on windows, maybe if I just try a little harder.

This is looking better, although std is still pretty hefty at 659,243 bytes, but it is at least sharable. However, I still have all those other imports which also concern me, they are just from std now instead of my program.

None of this is an issue with RUST the language, but I really think the RUST support libraries may need a little work before RUST is viable on Windows.

Any ideas why static linking makes things so big, even bigger than the DLL based versions (including the Dll sizes themselves)

Much better, now we are "in the ballpark" at 6 to 1, which is not so shabby for something brand new. I haven't researched eactlywhat the switches do, but I like them.

I'll press on with a more significant example as I refuse to give up yet.

To be clear, are you statically linking your C++ test to both msvcr and msvcp in order to fairly compare to the static Rust binary?

No. C won't go away. In fact, Rust has never been a direct competitor to C, though many uses of C would be better replaced with C++ and thus Rust.

The theoretically optimal compiler should compile println!("Hello, world!") into a series of CreateFile("CONOUT$") and WriteFile calls and nothing else, preferably. (In POSIX this will be alternatively write(1, ...) and exit().) It is very hard, though, because we are overlaying lots of abstractions over the raw OS APIs partially to avoid the platform dependency and also to ease the common usage. In the current Rust implementation, there are three major layers that add a binary bloat:

  1. C standard library (libc), which provides a stdout handle. It also has its own buffer (despite Rust has its own buffering!). Well, perhaps one can disable buffering with setvbuf, but that doesn't automagically remove the buffering code.
  2. The common I/O infrastructure via Writer. Actually this one is not that huge, but some generic uses of Writer need a trait object which is comparable to vtables in C++ and keeps otherwise unused methods alive.
  3. Formatting infrastructure (std::fmt). This one is perhaps the largest since it handles all kinds of formatting including integers and floating point numbers. In fact, msvcrt does not load the formatting routines for floating point numbers when possible (that's a cause of occasional R6002 error). Note that libc also suffers from this problem and statically linked libc is actually no better than iostream.

Note that 1 and 3 are known issues and I guess they will be fixed over the course. But that's already quite better than iostream, which has one more layer for internationalization support (dreaded std::codecvt and so on).

1 Like

Currently we get stdout on Windows via STDOUT_FILENO and using get_osfhandle to get a HANDLE which we WriteConsoleW to (unless stdout was redirected in which case we use WriteFile). Ideally we should be calling GetStdHandle(STD_OUTPUT_HANDLE).