In this lesson, you will learn how to bypass the performance tax of dynamic dispatch by devirtualizing function calls to enable aggressive compiler inlining.
The Cost of Indirection
When you use trait objects (&dyn Trait) or raw function pointers (fn()), the CPU must perform an indirect jump. It cannot predict the target address until runtime, which prevents the compiler from performing inlining—the process of embedding the function body directly into the caller. Without inlining, the compiler misses opportunities for constant propagation and further instruction scheduling optimizations.
Devirtualization via Enums
If the set of possible types is known at compile time, the most effective way to eliminate indirect calls is to replace trait objects with an enum. This allows the compiler to see every possible branch and inline the code accordingly.
Consider this inefficient pattern:
// Indirect call: The compiler doesn't know which 'draw' will run.
fn render(shape: &dyn Shape) {
shape.draw();
}
By switching to an enum, we give the compiler enough information to perform monomorphization and branch prediction:
enum Shape {
Circle(Circle),
Square(Square),
}
impl Shape {
#[inline(always)]
fn draw(&self) {
match self {
Shape::Circle(c) => c.draw(), // The compiler can inline this!
Shape::Square(s) => s.draw(), // And this!
}
}
}
Static Dispatch via Generics
If you are designing a high-performance library, prefer generics with trait bounds (<T: Trait>). Rust’s monomorphization creates a specialized copy of your function for every type used, turning what would be an indirect jump into a direct call instruction (or, more likely, a block of inlined code).
If you absolutely must use a function pointer (e.g., in FFI), check if you can use a jump table or a constant table strategy to group similar calls, helping the CPU’s Branch Target Buffer (BTB) predict the jump destination more accurately.
Key Takeaway: Replace dynamic dispatch with enums or generics wherever possible to provide the compiler with the necessary visibility to inline calls and eliminate runtime branch penalties.