Repository navigation
Oversized executables #747
Description
Activity
That's one of the reasons the multicall binary exists. As for individual binaries, I'm not sure what we can do except trying to reduce the number of dependencies.
The project could look into https://github.com/lrs-lang/lib, but it looks like a pretty big change and it may remove large amounts of cross-platform support.
@Hexel I did :) Unfortunately it's linux only.
The elephants in the room are static linking, ABI compatibility, and as you mentioned libstd.
Some demonstrations. all "results" blocks are generated using the following command:
strip main && stat --printf="%s bytes\n" main && ldd main | cut -d= -f1All of the results are going to be pretty specific to x86_64. I'm running debian stable.
All of the results below link against libc, the C standard library. You discuss suitability as a systems language. C is a system language, but most of its userland applications, like rust, use a standard library which takes up more than hundreds of kilobytes.
The difference as is demonstrated below, is that cargo by default does not use dynamic linking, because it's expected that most users will not (at this point in time) have an ABI-compatible rust standard library installed.
approaches
Rust, using libstd, static linking to libstd
command
echo 'fn main(){println!("Hello!\n");}' > main.rs rustc -C opt-level=3 -C lto main.rsresults
290864 bytes linux-vdso.so.1 (0x00007fff6fd88000) libpthread.so.0 libgcc_s.so.1 libc.so.6 /lib64/ld-linux-x86-64.so.2 (0x00007f278dfd8000)Rust, using libstd, dynamic linking to libstd
libstd is about 4.4mb and has to live on the operating system.
command
echo 'fn main(){println!("Hello!\n");}' > main.rs rustc -C opt-level=3 -C prefer-dynamic main.rsresults
5528 bytes linux-vdso.so.1 (0x00007ffc9c70b000) libstd-17a8ccbd.so libc.so.6 libdl.so.2 libpthread.so.0 libgcc_s.so.1 /lib64/ld-linux-x86-64.so.2 (0x00007f7e2c10c000) libm.so.6 librt.so.1Rust, no libstd framework
commands
wget https://raw.githubusercontent.com/rust-lang/rust/master/src/test/run-pass/smallest-hello-world.rs rustc -C opt-level=3 -C lto -o main smallest-hello-world.rsresults
4992 bytes linux-vdso.so.1 (0x00007ffeae1bf000) libc.so.6 /lib64/ld-linux-x86-64.so.2 (0x00007f63f0832000)C++, dynamic linking to libstdc++
Provided on my system by libgcc, which links directly against libm, so together they take up about 1.1mb total.
commands
echo -e '#include <iostream>\nint main() { std::cout << "Hello!\\n"; return 0; }' > main.cpp clang++ -Oz main.cpp -o mainresults
5400 bytes linux-vdso.so.1 (0x00007ffd4d58e000) libstdc++.so.6 libm.so.6 libgcc_s.so.1 libc.so.6 /lib64/ld-linux-x86-64.so.2 (0x00007f943e40b000)C
commands
echo -e '#include <stdio.h>\nint main() { printf("Hello!\n"); }' > main.c clang -Oz main.c -o mainresults
4616 bytes linux-vdso.so.1 (0x00007fff4363f000) libc.so.6 /lib64/ld-linux-x86-64.so.2 (0x00007fc53febd000)Can we just do everything dynamically
Imaginably if a distribution like debian or fedora has a libstdc++ that c++ programs can be compiled against, why not do this for rust?
Rust currently lacks ABI stability.
Well, it's the same and it isn't. C++ these uses a (relatively much more) stable ABI - it usually only changes with a major standards change. This means that when libstdc++ is compiled with a slightly newer version of clang you can install that on your system without also upgrading binaries that use libstdc++ and were compiled with an older version of clang.
When it has ABI stability, we'll be able to compete.
Rust doesn't have that yet, it's in the works as the Rust developers know it's needed to be able to ship binaries that are not so tightly coupled. But in the meantime, if you have a library that was complied with rustc v1.2 and you upgrade it and this new version is compiled with rustc v1.5, all binaries and libraries you were using that were linking against that library also now need to be replaced with versions of themselves compiled with rustc v1.5.
At some point in the future, there will be a stable ABI, and some systems will begin installing libstd as a dependency for some other tool. And for systems which have libstd, not only will the footprint of uutils coreutils per se is going to be a few hundred kb less, but we'll be able to painlessly split it into one binary per tool.
In the meantime
In the meantime, the best option is just as we're going right now - statically include parts of libstd that we need, then use a "multicall install" which imitates a binary for each tool we're providing via symbolic links.
In terms of speed, dynamic or static linking really has a negligible difference.
Reacted by Matt Ickstadt, Cengiz Can, yed, Simon THOBY, Yufan Lou, Ankit Goel, Brian Cain, Maximilian Hils, PotHix, Jinks and 15 morecreating linkback for #140
@nathanross Dynamic linking is not the answer, nor is multicall.
Rust programs already dynamically link to
libSystemon OS X, which provides the entire C standard library plus a multitude of other features. The solution is not to dynamically link libstd.The "features" libstd provides on top of libSystem are minimal—primarily structural—and in trivial programs ought to be removable. And indeed they can be, as LRS-lang demonstrates, but this requires undoing design flaws from Rust.
And multicall works rather poorly on windows. The solution instead is to judiciously code the binaries as close to the 80K minimum as possible.
@alexchandel: how would a C library help with providing e.g. Rust-style string formatting?
A viable solution on Windows might be multicall built as a dylib, plus a small stub binary that just calls the main entry point in that library. The latter could use #![no_std] to ensure the smallest possible size.
@vadimcn It doesn't need to, because string-formatting makes up relatively little of these binaries, and is inlined little with relative ease (once you stop panicking).
If you actually read the disassembly for PROFILE=release cp, you'll find that the largest symbols by far (at 34% of the text section) are
__ZN4copy20h24e32c79ba610ccdJmaEand__ZN6uumain20ha15e8d6f5b9ddecfceaE. And you'll notice that there are calls to a huge number of symbols that panic in unoptimizable ways. Many of these are from show_error, which makes two unoptimizable writes that inexplicably panic instead of silently failing or aborting, but there are just as many explicit panics.For comparison, I'm working on an
lsfor coreutils that never panics and obviously uses its own print/show_error macros, and it's barely 130K yet does far more thancp. I haven't even gotten around to optimizing it yet.Reacted by Yufan Loufascinating @alexchandel your continued investigation into, and passion about, this topic is greatly appreciated.
@alexchandel Have you done any additional test since this issue was last discussed? It has been more than 2 years now and rust has changed quite a lot. Would be interesting to see how the binary size was influenced by this.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Some notes from playing with the size:
I added:
cargo-features = ["strip"] # ... [profile.release] strip = "symbols" opt-level = 'z' lto = true codegen-units = 1 panic = 'abort'# commands I ended up using # make build-coreutils build-pkgs PROFILE=release # fd . --type x --max-depth 1 ./target/release/ -x du {} | sort -k 2,2 # did some manual formatting to help with readability 3844 arch -> 496 arch 4020 base32 -> 580 base32 4020 base64 -> 580 base64 3888 basename -> 512 basename 3924 cat -> 532 cat 3376 chgrp -> 328 chgrp 3964 chmod -> 544 chmod 3980 chown -> 548 chown 3948 chroot -> 548 chroot 3900 cksum -> 516 cksum 3892 comm -> 512 comm 14684 coreutils -> 4428 coreutils 4080 cp -> 620 cp 5572 csplit -> 1392 csplit 3964 cut -> 552 cut 3976 date -> 568 date 3972 df -> 560 df 3388 dircolors -> 340 dircolors 3876 dirname -> 508 dirname 3428 du -> 376 du 3880 echo -> 500 echo 3996 env -> 556 env 3912 expand -> 520 expand 3768 expr -> 684 expr 3372 factor -> 332 factor 3176 false -> 232 false 3944 fmt -> 540 fmt 3908 fold -> 520 fold 3880 groups -> 504 groups 5580 hashsum -> 1432 hashsum 3980 head -> 560 head 3272 hostid -> 280 hostid 3916 hostname -> 528 hostname 3916 id -> 520 id 4028 install -> 580 install 3944 join -> 556 join 3892 kill -> 516 kill 3864 link -> 500 link 3916 ln -> 532 ln 3860 logname -> 504 logname 5880 ls -> 1528 ls 3892 mkdir -> 516 mkdir 3876 mkfifo -> 504 mkfifo 3892 mknod -> 512 mknod 4000 mktemp -> 568 mktemp 3912 more -> 528 more 3960 mv -> 548 mv 3880 nice -> 512 nice 5424 nl -> 1316 nl 3888 nohup -> 512 nohup 3884 nproc -> 516 nproc 3936 numfmt -> 548 numfmt 4048 od -> 596 od 3892 paste -> 516 paste 3884 pathchk -> 512 pathchk 3948 pinky -> 540 pinky 3872 printenv -> 504 printenv 3320 printf -> 312 printf 5560 ptx -> 1412 ptx 3864 pwd -> 504 pwd 3884 readlink -> 516 readlink 3888 realpath -> 516 realpath 3892 relpath -> 516 relpath 3960 rm -> 544 rm 3872 rmdir -> 512 rmdir 3916 seq -> 536 seq 3972 shred -> 560 shred 3984 shuf -> 560 shuf 3872 sleep -> 512 sleep 4464 sort -> 740 sort 3988 split -> 568 split 3988 stat -> 564 stat 7212 stdbuf -> 868 stdbuf 3896 sum -> 516 sum 3868 sync -> 500 sync 3904 tac -> 520 tac 3928 tail -> 532 tail 3928 tee -> 520 tee 3228 test -> 260 test 3956 timeout -> 548 timeout 3920 touch -> 528 touch 3908 tr -> 516 tr 3176 true -> 232 true 3888 truncate -> 516 truncate 3924 tsort -> 524 tsort 3868 tty -> 500 tty 3872 uname -> 500 uname 3920 unexpand -> 520 unexpand 3972 uniq -> 560 uniq 3876 unlink -> 504 unlink 3960 uptime -> 564 uptime 3872 users -> 504 users 3908 wc -> 520 wc 3952 who -> 540 who 3840 whoami -> 496 whoami 3864 yes -> 500 yes Finished release [optimized] target(s) in 2m 04s -> Finished release [optimized] target(s) in 3m 41sFor multi call I tried out the performance of the size optimized version and it wasn't too bad but there's probably some tweaking to find a balance of size and performance
# make build-coreutils MULTICALL=y PROFILE=release $ hyperfine --runs 8 --warmup 2 "/coreutils-8.32/bin/ls -al -R ./linux > /dev/null" "./coreutils_0 ls -al -R ./linux > /dev/null" "./coreutils_1 ls -al -R ./linux > /dev/null" Benchmark #1: /coreutils-8.32/bin/ls -al -R ./linux > /dev/null Time (mean ± σ): 393.4 ms ± 15.9 ms [User: 173.5 ms, System: 219.5 ms] Range (min … max): 377.5 ms … 421.1 ms 8 runs Benchmark #2: ./coreutils_0 ls -al -R ./linux > /dev/null Time (mean ± σ): 410.2 ms ± 29.0 ms [User: 265.8 ms, System: 144.1 ms] Range (min … max): 377.2 ms … 464.8 ms 8 runs Benchmark #3: ./coreutils_1 ls -al -R ./linux > /dev/null Time (mean ± σ): 504.5 ms ± 39.4 ms [User: 358.7 ms, System: 145.5 ms] Range (min … max): 465.0 ms … 586.2 ms 8 runs Summary '/coreutils-8.32/bin/ls -al -R ./linux > /dev/null' ran 1.04 ± 0.08 times faster than './coreutils_0 ls -al -R ./linux > /dev/null' 1.28 ± 0.11 times faster than './coreutils_1 ls -al -R ./linux > /dev/null' $ du coreutils_* 14684 coreutils_0 4428 coreutils_121 remaining items
After #8513 , binary size with
release-fast,-Ctarget-cpu=native -Clink-arg=-z -Clink-arg=pack-relative-relocsand build-std is about 9.2 MB . (native is impossible to add to release).-Clink-arg=-z -Clink-arg=pack-relative-relocsis already used at some distribution, but is it Linux only?9.2 MB
from 10.1 MB, right ?
No. 10.1 is size without build-std hack. I'll rebuild
[Edit]
before uudoc commit: 9572048 byte -> main 9426352 byte
(under native, build-std hack)rust 1.92 changed default for unwind tables at
-C panic=abort. https://blog.rust-lang.org/2025/12/11/Rust-1.92.0/
So needed to add-Cforce-unwind-tables=yes. Can we add it toCargo.toml's release-{fast,small} profile?Reacted by Bennet Bleßmannrust 1.92 changed default for unwind tables at
-C panic=abort. https://blog.rust-lang.org/2025/12/11/Rust-1.92.0/ So needed to add-Cforce-unwind-tables=yes. Can we add it toCargo.toml's release-{fast,small} profile?I might be confused, but wouldn't one want to set
-Cforce-unwind-tables=noto get the old behavior back?
yesappears to be the new default if I am understanding https://blog.rust-lang.org/2025/12/11/Rust-1.92.0/#emit-unwind-tables-even-when-cpanic-abort-is-enabled-on-linux correctly. Or should it be set toyesexplicitly as that is the desired behavior and we want it to be used on older rust (<1.92) as well?Oh, I typo. I want to add
=nofor binary size.Linking #9776 for the people still depending on individual bins.
Size of individual
falseis quite important: #10243we made a lot of improvements, it feel that it is good enough now, closing
@sylvestre Do you still want to trace bin size on the CI for all binaries?
@oech3 yeah, it is still the case, no ?
I see. I was considering to
removemerge the size CI to another CI to make #10368 easier. (We are currently publishing individual bins from size CI which works only for 1 Linux-x64-glibc).I might extend size CI for all platforms when I moved it.
Size with
-C force-unwind-tables=nois
#12247 (comment)
The uutils executables are a bit larger than their native counterparts. These are the stats on OS X with O3, LTO, and alloc_system:
I think the funniest one is
nl, which is 6300% larger than the nativenl. jemalloc would've added another 230K to each of these.I realize some of this is Rust's fault: when an optimized, LTO'd, alloc_system'd
fn main(){println!("Hi!\n");}is still 84K, there's not much room. For example from the object dump/disassembly, about 9% of that dead weight was panicking code & string literals for the standard library :\ If we're really condemned to that, and to an 80K hello world, with all the implied overhead (and it's clearly to scale, as seen above), then this raises serious doubts about Rust as a system language.But surely we can shed some of the remaining 196K/216K/etc off of tr/tsort/friends? The median size of the native executables is 8.0K.