15.5Monomorphization

Last updated June 13, 2026

A nagging question has been waiting since lesson 15.1: if one generic function serves i32, f64, and char, what does the compiled program contain? Is there some runtime machinery that figures out the type on each call? The answer is no, and it's the reason Rust's generics are called zero-cost: the compiler turns your one generic definition into several specialized copies at compile time, so at runtime there's nothing generic left.

One definition, many compiled copies

The process is called monomorphization (from "mono", one, and "morph", form: turning one generic form into many concrete ones). When you write a generic function and call it with specific types, the compiler generates a separate, specialized version of the function for each type you actually use. Take this:

fn largest<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

fn main() {
    let a = largest(3, 7);          // used with i32
    let b = largest(2.5, 1.0);      // used with f64
    println!("{a} {b}");
}
7 2.5

(The T: PartialOrd bound is chapter 16's, included so this compiles.) You wrote largest once, but you called it with i32 and with f64. So the compiler produces, behind the scenes, roughly two functions:

fn largest_i32(a: i32, b: i32) -> i32 { if a > b { a } else { b } }
fn largest_f64(a: f64, b: f64) -> f64 { if a > b { a } else { b } }

and rewrites the calls to use the matching one. Your source has a single generic largest; the binary has one concrete copy per type used. This is exactly the hand-written duplication lesson 15.1 wanted to avoid, except the compiler writes the copies, not you, so there's one place to maintain and no chance of the copies drifting apart.

Why this is zero-cost

Because the specialization happens at compile time, the running program calls a normal, concrete function. There's no "what type is this?" lookup at runtime, no indirection, no overhead. largest(3, 7) runs exactly as fast as if you'd hand-written largest_i32 and called it directly. This is what "zero-cost abstraction" means: the generic code is a convenience for you, the programmer, and it costs nothing at runtime compared to the specialized code you'd otherwise write by hand.

Key insight

Monomorphization moves the cost of generics from runtime to compile time. Other languages often implement generics with runtime machinery (boxing values, looking up types), paying a small speed price on every generic call. Rust pays at compile time instead: it generates the specialized code once, during the build, so the running program is as fast as non-generic code. You get the abstraction for free where it counts, in the hot path of the running program.

The tradeoff: binary size and compile time

Nothing is truly free, and monomorphization's price is paid in two places, neither at runtime. First, binary size: if you use a generic function with ten different types, the compiler generates ten copies, and all ten end up in your executable. Heavy use of generics across many types can grow the binary (this is sometimes called "code bloat"). Second, compile time: generating and optimizing all those copies takes the compiler longer than compiling a single function would. This is part of why Rust builds aren't instant, and why cargo check (lesson 13.6), which skips code generation, is so much faster than cargo build.

For almost all programs this tradeoff is overwhelmingly worth it: a slightly larger binary that compiles a bit slower, in exchange for abstractions that cost nothing at runtime. When binary size genuinely matters (embedded systems, say) or when you need a single function to handle many types decided at runtime, there's an alternative that trades a little runtime speed for one shared copy: trait objects (dyn Trait, lesson 16.9). That's the other side of the same coin, and chapter 16 draws the comparison directly. For now, the default to remember is: generics are monomorphized, so reach for them freely and pay nothing at runtime.

Best practice

Use generics without worrying about runtime cost; that's the whole point of monomorphization. Only think about the binary-size tradeoff if you're building for a size-constrained target or you notice generics being instantiated across a very large number of types. In the rare case where you need one function body shared at runtime across many types (or to store mixed types in one collection), that's the signal to consider trait objects (lesson 16.9) instead.

Quiz time

Question #1

What does the compiler produce from a generic function called with three different types, and what is this process called?

Show solution

It generates three specialized copies of the function, one per concrete type used, and rewrites the calls to use the matching copy. The process is called monomorphization: turning one generic form into many concrete ones at compile time.

Question #2

Why are Rust's generics described as "zero-cost" at runtime?

Show solution

Because monomorphization specializes generic code at compile time, the running program calls ordinary concrete functions with no runtime type lookup or indirection. A generic call runs exactly as fast as the equivalent hand-written non-generic function. The abstraction costs you nothing at runtime; it's purely a compile-time convenience.

Question #3

What does monomorphization cost, if not runtime speed?

Show solution

Binary size (each type a generic is used with adds another compiled copy to the executable) and compile time (the compiler generates and optimizes all those copies). Both are paid at build time, not while the program runs. For most programs the tradeoff is well worth it; when it isn't, trait objects (lesson 16.9) offer a single shared copy at a small runtime cost.

That completes the generics machinery. The summary is next, and then chapter 16 delivers the payoff the whole chapter pointed at: traits, which let you finally tell the compiler "T is any type that can be compared/printed/added," finishing largest and unlocking the real power of generic code.