Generics

Monomorphization

Monomorphization is the compile-time process by which the Rust compiler transforms polymorphic generic code into concrete, single-type ("monomorphic") functions and data structures by duplicating and specializing each generic definition for every unique concrete type combination actually used in the program.

How the Compiler Specializes Generic Code

Consider a generic function compute_area<T> called with u32 and f64:

use std::ops::Mul;

fn compute_area<T: Copy + Mul<Output = T>>(width: T, height: T) -> T {
    width * height
}

fn main() {
    let pixel_area = compute_area(1920_u32, 1080_u32);
    let meter_area = compute_area(4.5_f64, 3.2_f64);

    println!("Pixels: {pixel_area}");
    println!("Meters: {meter_area:.2}");
}
Pixels: 2073600
Meters: 14.40

During compilation, rustc scans all call sites, collects the concrete instantiations T = u32 and T = f64, and emits two distinct functions into LLVM IR—equivalent to having written:

Source Code (Generic)                  Monomorphized Code (Compiler-Generated)
+--------------------------------+     +------------------------------------------+
| fn compute_area<T>(w: T, h: T) | --> | fn compute_area_u32(w: u32, h: u32)->u32 |
|   where T: Copy + Mul<Output=T>|     |     { w * h }  // integer MUL instruction|
| {                              |     +------------------------------------------+
|     w * h                      | --> | fn compute_area_f64(w: f64, h: f64)->f64 |
| }                              |     |     { w * h }  // float FMUL instruction |
+--------------------------------+     +------------------------------------------+

Dead Generic Instantiations Are Never Emitted

If a generic function is defined in a crate or module but never called with a concrete type, or if compute_area<i128> is never referenced anywhere in the dependency graph, the compiler generates zero machine code for that unused instantiation.

Furthermore, each concrete instantiation gets its own distinct function address in the compiled binary (unless merged by an LLVM identical-code-folding optimization pass when machine instructions are bit-for-bit identical):

fn identity<T>(x: T) -> T {
    x
}

fn main() {
    let fn_u8: fn(u8) -> u8 = identity::<u8>;
    let fn_u64: fn(u64) -> u64 = identity::<u64>;

    println!("identity::<u8>  address: {:p}", fn_u8 as *const ());
    println!("identity::<u64> address: {:p}", fn_u64 as *const ());
}
identity::<u8>  address: 0x1024bc3f0
identity::<u64> address: 0x1024bc400

Benefits and Trade-Offs of Monomorphization

Dimension Effect of Monomorphization
Runtime dispatch Direct static function calls (call instruction) with no vtable lookup
Inlining & optimization LLVM can inline compute_area_u32 directly into the caller and constant-fold or vectorize loops
Memory layout Generic structs (Vec<u8>, Option<u32>) are laid out tightly in memory with exact concrete field sizes
Binary size ("code bloat") Calling a large generic function with 20 different concrete types emits 20 copies of the machine code
Compile time Downstream crates must monomorphize and optimize every concrete instantiation they use

Reducing Monomorphization Bloat with the Non-Generic Inner Function Pattern

When a generic function takes a type conversion parameter (such as P: AsRef<Path> or S: Into<String>) and then executes a large body that does not depend on P, the compiler would normally duplicate the entire large body for &str, String, PathBuf, and &Path.

To avoid duplicating the heavy body, keep only the thin conversion wrapper generic and delegate immediately to a private non-generic inner function:

use std::path::Path;

// Thin generic wrapper: monomorphized per caller type (`&str`, `PathBuf`, etc.),
// but compiles down to a trivial slice conversion + jump.
pub fn inspect_path<P: AsRef<Path>>(path: P) -> usize {
    inspect_path_inner(path.as_ref())
}

// Non-generic core logic: compiled into machine code exactly ONCE.
fn inspect_path_inner(path: &Path) -> usize {
    let components = path.components().count();
    let is_abs = path.is_absolute();
    println!("Path {:?}: components = {}, absolute = {}", path, components, is_abs);
    components
}

fn main() {
    inspect_path("/usr/local/bin");
    inspect_path(std::path::PathBuf::from("src/main.rs"));
}
Path "/usr/local/bin": components = 4, absolute = true
Path "src/main.rs": components = 2, absolute = false