199. Static Dispatch — Generics Beat Box<dyn Trait> When You Can Afford the Code
Box<dyn Trait> is the reflex when a function “takes something that implements a trait.” But every call through it pays for a vtable hop the compiler can’t see past. Swap it for a generic and the optimizer inlines the whole thing.
This is the morning half of a pair with this afternoon’s #[inline] & Copy bite: both are about giving the optimizer a body it can actually fold into the caller.
The cost: Box<dyn Trait> hides the call from the optimizer
When you accept Box<dyn Fn(i32) -> i32> (or any dyn Trait), the concrete type is erased. At each call site the program loads a function pointer from a vtable and jumps through it. The compiler has no idea what’s on the other end, so it can’t inline the body, can’t constant-fold through it, can’t vectorize a loop around it:
| |
There’s also a heap allocation just to hold the closure, and the pointer-chase ruins instruction-cache locality in a hot loop.
The fix: a generic parameter monomorphizes to the real type
Take impl Fn (sugar for a generic) instead. The compiler stamps out a specialized copy of apply_all for each concrete f you pass — monomorphization. Inside that copy the closure’s body is fully visible, so it gets inlined and the map loop optimizes as if you’d written the arithmetic by hand:
| |
No box, no vtable, no allocation. impl Trait in argument position is the same thing with less typing:
| |
| |
Both generic versions compile to a tight loop with the multiply spliced straight in.
The same trick for returns: impl Trait instead of Box<dyn>
Returning a dyn value forces a box too. If the function only ever returns one concrete type, impl Trait keeps it static — the caller gets the real type and can inline through it:
| |
When dyn is still the right call
Static dispatch trades code size for speed: each instantiation is a fresh copy, so monomorphizing over many types bloats the binary. And generics can’t do heterogeneous collections — Vec<Box<dyn Draw>> holding circles and squares genuinely needs dynamic dispatch, because the element type varies at runtime. Reach for dyn when you need a uniform type for mixed values, want to shrink compile times and binary size, or the call isn’t hot enough to matter. Reach for generics / impl Trait when the call sits in a loop and you want the optimizer to see through it.
| |
Rule of thumb: default to a generic, and downgrade to dyn only when you have a reason — mixed types, code-size pressure, or a cold path where the indirection is free.