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