Rust Monomorphisation
Monomorphisation is the compiler building one specialised copy of generic code for each concrete type in use. One written largest becomes a separate copy for i32, for char, and for each other called type.
What the compiler writes for you
You write one largest. After monomorphisation, the program effectively holds one copy per called type — as if you had written these by hand:
fn largest_i32(list: &[i32]) -> &i32 { /* the same body, with T = i32 */ }
fn largest_char(list: &[char]) -> &char { /* the same body, with T = char */ }Proof one: the copies live at different addresses
A function can be turned into a value — the address of its code in the compiled program. Same type twice: one shared copy. A different type: a different copy.
let nums_a = largest::<i32> as fn(&[i32]) -> &i32;
let nums_b = largest::<i32> as fn(&[i32]) -> &i32;
let decimals = largest::<f64> as fn(&[f64]) -> &f64;
println!("i32 copy vs itself: same code = {}", std::ptr::fn_addr_eq(nums_a, nums_b));
println!("i32 copy vs f64 copy: same code = {}", std::ptr::fn_addr_eq(nums_a, decimals));Proof two: count the copies in the binary
Build a program that calls largest with three types, then list the functions inside the binary with the nm tool. One generic function in your source gives three largest symbols in the binary. The letter after the name is the type each copy was built for:
// in your terminal, after building:
// $ nm my_program | grep largest
// ... 7largestc... (the char copy)
// ... 7largestd... (the f64 copy)
// ... 7largestl... (the i32 copy)Share the heavy part to keep copies small
A common trick when copies grow: let the generic function do one small step. Here that step turns T into a &[u8]. Put the real work in one ordinary function. Every copy is then a thin wrapper, and the heavy code exists exactly once:
fn byte_total<T: AsRef<[u8]>>(data: T) -> usize {
byte_total_inner(data.as_ref())
}
fn byte_total_inner(bytes: &[u8]) -> usize {
let mut total = 0;
for b in bytes {
total += *b as usize;
}
total
}Example
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut best = &list[0];
for item in list {
if item > best {
best = item;
}
}
best
}
fn main() {
println!("largest numbers = {}", largest(&[3, 9, 4]));
println!("largest letters = {}", largest(&['a', 'z', 'm']));
println!("largest decimals = {}", largest(&[1.5, 9.25, 2.0]));
let nums_a = largest::<i32> as fn(&[i32]) -> &i32;
let nums_b = largest::<i32> as fn(&[i32]) -> &i32;
let decimals = largest::<f64> as fn(&[f64]) -> &f64;
println!("i32 copy vs itself: same code = {}", std::ptr::fn_addr_eq(nums_a, nums_b));
println!("i32 copy vs f64 copy: same code = {}", std::ptr::fn_addr_eq(nums_a, decimals));
}Key points
- Monomorphisation: while compiling, the compiler replaces each generic item with one copy per concrete type it is actually used with
- Only used combinations are built: a generic function that is never called with f64 produces no f64 copy
- Afterwards each copy is an ordinary function.
- Calls to it are direct.
- The optimiser may inline a copy into its caller.
- The price is paid while compiling and in binary size.
- More used types mean more code to build once.
- They also mean more code to ship.
- A generic struct copies too: Point<i32> and Point<f64> are two distinct types inside the compiled program
- After compilation there is no generic code left — only the copies.
In this example, main calls one written function, largest, with three different types: numbers, letters and decimals. Each call prints its winner — 9, z and 9.25. Below, the program turns the function itself into values. nums_a and nums_b are both the i32 copy, and fn_addr_eq says they are the same code: true. decimals is the f64 copy, and the same test against the i32 copy says false. Two distinct blocks of machine code exist in this small program, both born from the single largest you wrote.
Note
Nothing about the generic types is remembered while the program runs.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Your program calls one generic function with four different types. How many copies of it does the compiled program hold?