Skip to Content
Course content

237: Reducing Binary Size

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember the first time I shipped a small CLI utility written in Rust. I had a few dependencies and maybe a hundred lines of logic, but when I looked at the binary, it was nearly 10MB. For a tool that basically just checks a config file and exits, that felt absurd. The good news is that Rust's defaults are tuned for compilation speed and debugging, not for the smallest possible footprint. We can lean on the compiler to trim the fat.

Why is my tiny CLI tool so huge?

If you just ran cargo build --release, you're still carrying a lot of baggage. By default, Rust includes debug symbols—even in release mode—and it optimizes for speed (opt-level = 3) rather than size. Speed optimizations often involve "inlining" functions, which is great for performance but means the same code gets copied into multiple places in your binary.

Let's say you're building a simple tool that reads a file's metadata. Even with no complex logic, the standard library and the way Rust handles panics add a baseline of weight. Most of that "bloat" isn't actually executing code; it's metadata used by debuggers to tell you exactly which line crashed.

What should I actually put in my Cargo.toml to shrink things?

You don't need to hunt through obscure compiler flags. Most of what you need lives in the [profile.release] section of your Cargo.toml. I usually start with these three settings:

[profile.release]
opt-level = "z"     # Optimize for size. "s" is also an option, but "z" is more aggressive.
lto = true          # Enable Link Time Optimization. This lets the compiler see across crate boundaries.
codegen-units = 1   # Reduces parallelism in the build to allow more global optimizations.

LTO (Link Time Optimization) is the big winner here. Normally, Rust compiles each crate independently. LTO tells the compiler, "Wait until the very end, look at the whole program, and throw away any function that is never actually called anywhere." It makes your build take longer, but the size drop is usually significant.

Does switching to panic = "abort" actually help?

Yes, and more than you'd think. By default, when a Rust program panics, it "unwinds" the stack. This means it walks back through every function call, cleaning up memory and running destructors. To do this, the compiler has to embed "unwind tables"—basically a map of how to clean up the stack at every single point in your code.

If you're building a small tool where a panic is a fatal error anyway, you don't need that map. You can just crash immediately. Add this to your release profile:

[profile.release]
panic = "abort"

I've seen this shave off several hundred kilobytes on its own because it completely removes the need for those landing pads and unwind tables. Just keep in mind that catch_unwind will no longer work, but for 99% of CLI tools, that's a trade-off you'll happily make.

What about the strip command?

Even after the profile tweaks, your binary might still have some symbol information. You can remove this using the strip utility from your system, or if you're on a recent version of Rust (1.59+), you can let Cargo do it for you directly in the config:

[profile.release]
strip = true # Automatically strips symbols from the binary

This is the "nuclear option" for size. It removes the symbol table entirely, meaning if the program crashes, your stack trace will be a series of hexadecimal addresses instead of function names. In a production environment, you usually have a separate debug build for that, so stripping the production binary is standard practice.




📋 Practical Task

Shrinking the File-Metadata Utility

You have been given a small project that calculates the total size of all files in a directory. Currently, the release binary is unnecessarily large (around 4MB on some systems) because it's using default release settings.

Your Goal: Modify the Cargo.toml to implement the most aggressive size-reduction strategies discussed in this lesson.

Requirements:

  • Change the optimization level to prioritize the smallest possible binary.
  • Enable Link Time Optimization (LTO).
  • Restrict the code generation units to 1 to maximize optimization potential.
  • Set the panic strategy to abort to remove unwind tables.
  • Configure Cargo to automatically strip symbols from the final binary.

Once you've updated the Cargo.toml, run cargo build --release and compare the file size of the resulting binary in target/release/ to a version built with default settings.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.