On this page

Generics and Monomorphization

The core mechanism of Rust generics is monomorphization—the compiler generates independent machine code for each concrete type parameter at compile time. This means Vec<i32> and Vec<String> are two completely separate function bodies, with zero runtime overhead, at the cost of binary bloat. const generics also incorporate constant values into generic parameters, while specialization allows providing better implementations for specific types (currently nightly only).

Monomorphization: Generating Code for Each Concrete Type at Compile Time

Rust generics do not use runtime polymorphism (no vtables)—the compiler generates independent machine code for each concrete type used with generics:

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

let _ = max(1, 2);                   // Compiler generates max_i32
let _ = max(1.0, 2.0);              // Compiler generates max_f64
let x = "a"; let y = "b";
let _ = max(x, y);                  // Compiler generates max_&str
// The output binary contains 3 independent max functions, each optimized for a specific type

This is the same mechanism as C++ templates—both generate code at compile time. However, in Rust, generics are checked at definition time (no need to wait for instantiation), so error messages are clearer. C++ template errors usually appear at instantiation, leading to dozens of lines of template error output.

The Meaning of "Zero-Cost Abstraction"

In release builds, generic functions are usually inlined at the call site—in the final assembly, the generic function itself disappears, leaving only a few cmp instructions that directly operate on i32 or f64. This is what Rust calls "zero-cost abstraction": you can use high-level abstractions, but the compiled code is as fast as hand-written code for specific types.

Code Bloat and Mitigation

Generating code for each concrete type → many type combinations → explosion in compile time + binary size. A classic mitigation strategy: extract non-generic inner functions:

fn process<T: Display>(items: &[T]) {
    for item in items {
        // Generic part: convert to String (each T calls a different Display::fmt)
        let s = item.to_string();
        // Non-generic part: subsequent logic does not depend on T
        process_inner(&s);           // Compiled only once
    }
}
fn process_inner(s: &str) { ... }    // Does not depend on T → no branching

Key point: Separate the parts that must be generic (type conversion) from the parts that can be handled with trait objects or concrete types (business logic).

Const Generics: Values as Generic Parameters

Traditional generic parameters are types. const generics (Rust 1.51+) allow values to also be generic parameters—compile-time constants:

struct Buffer<T, const N: usize> { data: [T; N] }

let b1: Buffer<i32, 4> = Buffer { data: [1, 2, 3, 4] };
let b2: Buffer<i32, 8> = Buffer { data: [1; 8] };
// b1 and b2 are **different types**! They cannot be assigned to each other

Main use cases: fixed-size arrays/vectors, SIMD lanes, compile-time assertion of array size.

Specialization (nightly, not yet stable)

Current stable Rust does not support trait specialization—you cannot provide a "more specific impl that overrides a more general impl" for a trait. This limits certain performance optimizations (e.g., providing a more efficient Extend impl for Vec<u8> than for generic Vec<T>). min_specialization is available on nightly, but not yet on stable. The current workaround is to use the newtype pattern to provide specialization.

References

  • Rust Book: Chapter 10.1
  • Rust Reference: Const generics
  • RFC 2000: Const generics; RFC 1210: Specialization

Keywords: monomorphization, zero-cost abstraction, code bloat, const generics, specialization