Course contentsShow
← Rust
Lesson 1801 of 1,91015. Performance and Architectural EngineeringFree lesson

Avoiding Unnecessary Code Monomorphization

Control excessive generic expansion to reduce binary size and cache pressure.

In this lesson, you’ll learn how to curb the "code bloat" caused by Rust’s aggressive monomorphization, keeping your binary slim and your instruction cache happy.

The Cost of Convenience

When you define a generic function in Rust, the compiler creates a specialized copy of that function for every unique concrete type used as a type parameter. This is called monomorphization. While it allows for high-performance static dispatch, it has a hidden cost: if you use a generic function with many different types across your crate, the compiler generates a massive amount of redundant machine code. This increases your binary size and leads to "instruction cache thrashing," where the CPU spends more time fetching instructions from memory because the hot code no longer fits in the cache.

Controlling the Explosion

To mitigate this, you should move logic that doesn't strictly depend on the generic type out of the generic function. Consider a generic Processor<T>:

// A "heavy" generic function that causes bloat
fn process_data<T: DataTrait>(data: &T) {
    // 100 lines of complex logic that doesn't touch T...
    complex_setup_routine(); 
    // ...that gets duplicated for every T!
    data.execute();
}

By splitting this, you only monomorphize the small part that actually changes:

fn process_data<T: DataTrait>(data: &T) {
    non_generic_setup(); // Compiled once!
    execute_inner(data); // Only this small part is monomorphized
}

fn non_generic_setup() { /* ... */ }

fn execute_inner<T: DataTrait>(data: &T) {
    data.execute();
}

When to use dyn

If you find yourself with an explosion of types that all implement the same trait, consider whether you actually need the performance of static dispatch. Using &dyn DataTrait (dynamic dispatch) replaces multiple monomorphized copies with a single vtable-based function. While dynamic dispatch incurs a tiny runtime overhead for pointer indirection, it often drastically improves performance in large systems by significantly reducing the binary footprint and improving instruction cache locality.

Key Takeaway: Extract non-generic logic into separate functions and favor dynamic dispatch when code bloat outweighs the micro-performance gains of static monomorphization.