clear todos for tiny rust binaries
This commit is contained in:
parent
d5e0d8c84e
commit
718163d969
|
@ -1,29 +1,27 @@
|
|||
+++
|
||||
date = '2025-04-07T20:29:48+02:00'
|
||||
draft = true
|
||||
title = 'Debloating your rust binary'
|
||||
title = 'Tiny Rust binaries'
|
||||
tags = ['rust', 'servicepoint']
|
||||
+++
|
||||
|
||||
<!-- TODO: is it rust or Rust -->
|
||||
|
||||
In [CCC Berlin](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wall that we call "ServicePoint display".
|
||||
Anyone in the local network can send UDP packets containing commands that the display will execute.
|
||||
The commands are sent in a binary data structure and contain things like very basic text rendering and overwriting parts of the pixel buffer.
|
||||
I wrote (most of) the rust library [servicepoint](https://crates.io/crates/servicepoint), which implements serialisation and deserialisation of those packets.
|
||||
I wrote (most of) the Rust library [servicepoint](https://crates.io/crates/servicepoint), which implements serialisation and deserialisation of those packets.
|
||||
There are also bindings for other languages, [including C](https://git.berlin.ccc.de/servicepoint/servicepoint-binding-c).
|
||||
|
||||
Some weeks ago, the only user of those C bindings I know said they'll stop using it, with a big grin on their face.
|
||||
While I know from experience that writing such a library is great fun (and thus does not need another reason), I immediately wanted to know why.
|
||||
The main reason they cited was binary size, and while there's probably something wrong with your computer if you do not have 1MB to spare, I agreed that it was too big for what it does.
|
||||
Thus, I was immediatedly nerd-sniped and I could not think about anything else in my spare time for a whole week.
|
||||
It was an evil nerd-snipe and I could not think about anything else in my spare time for a whole week.
|
||||
I _had_ to find out why it was so big, and there would _have_ to be a way to fix it.
|
||||
|
||||
This is part one, where I optimize the core library for size.
|
||||
The order in which I tried all the options is changed for a better text structure, but the results are re-created in the order they appear using the stated tools.
|
||||
In a future post, I also want to document how I got the C bindings smaller, as those use all features by default.
|
||||
|
||||
Most of the techniques I used are descibed in [Minimizing Rust Binary Size](https://github.com/johnthagen/min-sized-rust), though I hope the specific example I provide makes the topic interesting to readers not writing rust code.
|
||||
Most of the techniques I used are descibed in [Minimizing Rust Binary Size](https://github.com/johnthagen/min-sized-rust), though I hope the specific example I provide makes the topic interesting to readers not writing Rust code.
|
||||
|
||||
Let's get hacking!
|
||||
|
||||
|
@ -102,7 +100,7 @@ The resulting size was 1.1 MB, which should be easy enough to beat.
|
|||
|
||||
### Compiler options
|
||||
|
||||
The first thing that came to mind was `-Os`, so compiling for binary size. The rust equivalent is `opt-level = "s"`, or `z` to also disable loop vectorization.
|
||||
The first thing that came to mind was `-Os`, so compiling for binary size. The Rust equivalent is `opt-level = "s"`, or `z` to also disable loop vectorization.
|
||||
|
||||
| Option | size in isolation (change) | size cumulative (change) |
|
||||
| - | - | - |
|
||||
|
@ -115,7 +113,7 @@ The first thing that came to mind was `-Os`, so compiling for binary size. The r
|
|||
| strip = true | 915.944 | 580.056 |
|
||||
| switching back to opt-level = 'z' | | 555.480 |
|
||||
|
||||
So it turns out, if you want to halve your binary size, a few flags are enough in stable rust. Also, the combinations of those settings do not work linearly, and sometimes what resulted in a smaller binary before now increased the size.
|
||||
So it turns out, if you want to halve your binary size, a few flags are enough in stable Rust. Also, the combinations of those settings do not work linearly, and sometimes what resulted in a smaller binary before now increased the size.
|
||||
The only compromise apart from compilation time is the change in panic behavior, as this means no stack traces on crash[^panic-abort].
|
||||
|
||||
To only compile like this in specific szenarios, you can add a new profile to a crates `Cargo.toml` like this:
|
||||
|
@ -282,7 +280,7 @@ Drumroll... 324.624 Bytes!
|
|||
40% of the binary was argument parsing.
|
||||
This also makes the main disappear from the top sized functions.
|
||||
|
||||
While removing a library you do not really need is also available in stable rust, I was only able to notice that with tooling only available on nightly, so I am putting it into that category.
|
||||
While removing a library you do not really need is also available in stable Rust, I was only able to notice that with tooling only available on nightly, so I am putting it into that category.
|
||||
|
||||
### 3. build-std
|
||||
|
||||
|
@ -328,7 +326,7 @@ You'd think that now `main` is the top function, but `Iter::next` is now the big
|
|||
Still, `[Unknown] main` and the actual main take up 10% of the remaining size according to `cargo bloat`.
|
||||
|
||||
We surely cannot reduce that, right? Wrong!
|
||||
With #[no_main], you can tell rust to not add any initialization code.
|
||||
With #[no_main], you can tell Rust to not add any initialization code.
|
||||
This means the normal `fn main()` does not get used, and the linker complains about the missing function.
|
||||
To fix this, the function can be converted to a C-style main.
|
||||
|
||||
|
@ -363,8 +361,8 @@ If we were to remove the marked line and not clear the screen, we could drop it
|
|||
|
||||
There are two things left to reach the absolute bottom without ripping out the standard libary alltogether.
|
||||
|
||||
In rust, a function can tell the compiler to get the calling location as a parameter to the function.
|
||||
With `-Zlocation-detail=none`, we instruct the rust compiler to just not bother with that.
|
||||
In Rust, a function can tell the compiler to get the calling location as a parameter to the function.
|
||||
With `-Zlocation-detail=none`, we instruct the Rust compiler to just not bother with that.
|
||||
|
||||
`-Zfmt-debug=none` is similar but worse, because it changes all the default `Debug` implementations to do nothing at all.
|
||||
The change in behavior is not obvious in this example, but do this in an application that has logging and it will be horribly broken.
|
||||
|
@ -388,7 +386,14 @@ All of this reduces the final binary size to 7.696 bytes.
|
|||
|
||||
## Conclusion
|
||||
|
||||
<!-- TODO -->
|
||||
Through this journey, I've managed to reduce the size of the example Rust binary using the servicepoint library from an unwieldy 1.1 MB to an impressive 7.7 KB, using all the options and tools I was able to find for that, without removing the standard library.
|
||||
For me the most unexpected was the size of `clap` code, though I learned dozens of things at every step about the intricacies of how `cargo build` produces your final binary.
|
||||
|
||||
There is no single option that in itself is the solution, it's all about the mix.
|
||||
While extreme options can be great if you want to squeeze out the last bytes, it's probably not worth using those in a "normal" computer scenario.
|
||||
|
||||
The key takeaway is that optimizing binary size in Rust, while not always straightforward, is achievable with the right techniques.
|
||||
It is certainly easier to create a big binary than in C, calling Rust bloated is blaming the language a bit too much.
|
||||
|
||||
[^1]: Yes, I know UDP does not have connections. Internally, this just opens a UDP socket
|
||||
[^panic-abort]: Technically, you can catch a panic while unwinding and there may even be a weird performance argument for doing that, see <!-- TODO find article about making serde faster with panic catching -->
|
||||
|
|
Loading…
Reference in a new issue