Skip to Content
Course content

238: Link-Time Optimization (LTO) Configuration

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

I've been profiling this data-processing tool I'm working on, and something is bugging me. I have a separate crate called fast_math that handles some heavy lifting—specifically, a tight loop performing coordinate transformations. Even in --release mode, the profiler shows a surprising amount of time spent jumping back and forth between my main binary and that utility crate. It's not a massive bottleneck, but it's inefficient.

The "Inline" Illusion

My first instinct was to just slap #[inline] on the transformation functions in fast_math. I did that, pushed the code, and ran the benchmark again. The result? Almost no change.

Here is the catch that trips a lot of people up: #[inline] is a suggestion, not a command. More importantly, when Rust compiles your project, it usually compiles each crate independently. By the time the linker gets involved to stitch the fast_math library and the main binary together, the opportunity to inline that function into the main loop has already passed. The compiler has already finished its optimization pass for the individual units.

// In fast_math/src/lib.rs
#[inline]
pub fn transform_coord(x: f64, y: f64) -> f64 {
    (x * 0.707) + (y * 0.707) // Simplified math for the example
}

If I want the compiler to actually look across the boundary of these two crates and say, "Hey, I can just shove the logic of transform_coord directly into the main loop," I need Link-Time Optimization (LTO).

Turning the Dial to "Fat"

Let's see what happens when we tell Cargo to go all out. I'll head into my Cargo.toml and add this to the release profile:

[profile.release]
lto = true

When I run cargo build --release now, I notice something immediately: the "Linking" stage is taking significantly longer. Like, way longer. It's not just a few seconds; it's a noticeable pause where my CPU fans start spinning up. This is "Fat LTO".

With lto = true (or "fat"), the compiler doesn't just optimize crates individually; it generates a massive amount of intermediate representation for everything and then performs a global optimization during the linking phase. The performance gain on my coordinate loop was about 8%—not huge, but measurable—because the compiler finally decided to inline those calls across the crate boundary.

Finding a Middle Ground

The problem is that as the project grows, "Fat" LTO becomes a nightmare for build times. I don't want to wait three minutes for a linker to finish every time I make a tiny change to a leaf crate. I wondered if there was a way to get most of the benefit without the agonizing wait.

I switched the config to "thin":

[profile.release]
lto = "thin"

I ran the build again. The linking phase was suddenly much faster—almost back to normal—but the execution speed of my program remained nearly identical to the "Fat" version. Thin LTO achieves this by performing optimizations in parallel and using a more lightweight summary of the crates rather than the full representation. In my experience, unless you are squeezing every single clock cycle out of a tiny embedded system, "thin" is almost always the right choice.

The Trade-off Matrix

Looking back at the three states I've tried, here is how I think about it now when I'm configuring a new project:

  • lto = false (Default): Fast builds. Each crate is an island. Use this for development and most standard releases where performance is "good enough."
  • lto = "thin": A great balance. You get cross-crate inlining and dead-code elimination with a modest hit to build time. This is my new default for production.
  • lto = "fat": Maximum optimization. The compiler sees everything. Use this for final release candidates or performance-critical libraries where build time doesn't matter, but binary size and speed do.



📋 Practical Task

Optimizing the High-Frequency Signal Processor

You have been handed a project consisting of two crates: signal_proc (a library that handles Fourier transforms) and signal_app (the binary that feeds data into the library). The app is currently too slow for real-time processing because the core transformation function is being called 10 million times per second across the crate boundary.

Your Task:

  1. Open the Cargo.toml of the signal_app crate.
  2. Configure the [profile.release] section to enable "Thin" LTO to improve cross-crate inlining without causing excessive build times.
  3. Additionally, set codegen-units = 1 in the same profile. (This forces the compiler to optimize the crate as a single unit, which often complements LTO by providing more optimization opportunities).
  4. Run cargo build --release and verify that the build completes successfully.

Expected Configuration:

[profile.release]
lto = "thin"
codegen-units = 1
Rating
0 0

There are no comments for now.

to be the first to leave a comment.