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

Dynamic vs Static Dispatch Trade-offs

Choose between vtables and generics based on the cost of branch prediction and inlining.

In this lesson, you will learn how to decide between static and dynamic dispatch by analyzing the hidden performance costs of branch prediction and inlining opportunities.

The Mechanics of Dispatch

When you use generics (e.g., fn process<T: Trait>(item: T)), the compiler performs static dispatch. It generates a specialized version of the function for every concrete type used. Because the compiler knows exactly which code is running, it can aggressively inline the function calls, effectively collapsing the abstraction and allowing the CPU to see the entire instruction stream.

Conversely, using trait objects (e.g., &dyn Trait) results in dynamic dispatch. The compiler creates a vtable (virtual method table) containing pointers to the trait methods. At runtime, the program must look up the correct address in the vtable before jumping to the code.

The Hidden Cost: Branch Prediction

While the indirection of a vtable is a minor cost, the architectural impact is significant. In static dispatch, the instruction pointer follows a predictable, linear path, which is ideal for the CPU’s instruction cache and branch predictor.

Dynamic dispatch, however, introduces indirect branches. If your code processes a collection of mixed types, the CPU cannot predict which specific implementation will run next. When the branch predictor misses, the CPU must flush its pipeline, causing a substantial latency spike.

Consider a tight loop:

// Static: Compiler can inline and unroll, perfect for branch prediction
for item in &items { item.execute(); }

// Dynamic: CPU must perform a pointer dereference and branch misprediction penalty
for item in &dyn_items { item.execute(); }

Choosing Your Strategy

  • Use Generics when the performance is critical, the loop is hot, and you want to enable cross-function optimization. The tradeoff is larger binary sizes due to "monomorphization."
  • Use Trait Objects when you need to store different types in a single collection or when you want to minimize compile times and binary size by sharing code across types. If the logic inside your trait method is heavy (e.g., a complex syscall), the overhead of the vtable becomes negligible compared to the work being performed.

Key Takeaway: Choose static dispatch for hot, latency-sensitive loops to enable inlining and avoid pipeline-stalling branch mispredictions; reserve dynamic dispatch for high-level architectural flexibility where performance is not bound by instruction throughput.