In this lesson, you will learn how dynamic dispatch via trait objects introduces overhead and how it impacts your program's ability to optimize code.
The Mechanism of Dynamic Dispatch
When you use a type like &dyn Trait, Rust employs dynamic dispatch. Because the compiler cannot know the concrete type at compile time, it uses a vtable (virtual method table). A trait object is essentially a fat pointer: one word points to the data, and another points to the vtable containing the function pointers for that type’s trait implementation.
Every time you call a method on dyn Trait, the CPU must perform an extra memory dereference to fetch the function pointer from the vtable before it can execute the code.
The Performance Penalty
While a single pointer dereference sounds cheap, the real cost lies in what the compiler cannot do. When using static dispatch (generics with trait bounds like T: Trait), the compiler uses monomorphization. It creates a specific copy of the function for every type T used, allowing it to see the concrete code path, perform inlining, and optimize across function boundaries.
With dynamic dispatch, the compiler treats the call as a "black box." It cannot inline the function, which prevents further optimizations like constant folding or loop vectorization.
Consider this comparison:
// Static Dispatch: Compiler can inline add()
fn process_static<T: Calculator>(calc: T, val: i32) {
calc.add(val);
}
// Dynamic Dispatch: Compiler must perform a vtable lookup
fn process_dynamic(calc: &dyn Calculator, val: i32) {
calc.add(val);
}
In process_dynamic, the add call is hidden behind a pointer. The CPU also faces a potential branch misprediction penalty, as it cannot easily predict the target of the jump, especially if the trait object is part of a heterogeneous collection (e.g., Vec<Box<dyn Calculator>>).
When working in performance-critical loops, replace dyn Trait with generics where possible to regain the benefits of inlining. Dynamic dispatch is excellent for building flexible APIs, but it forces the CPU to do the work at runtime that the compiler could have handled during the build.
Key Takeaway: Dynamic dispatch trades away the compiler’s ability to inline and optimize code by introducing runtime pointer indirection and vtable lookups.