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.