Table of contents
Performance Overhead
Closures in Rust have zero runtime overhead in most cases due to static dispatch and compiler optimizations. However, specific scenarios can introduce costs:
| Aspect | Closures | Regular Functions |
|---|---|---|
| Dispatch | Static (via monomorphization) | Always static (direct call) |
| Memory | May store captured data (size varies) | No captured data (fixed size) |
| Heap Allocation | Only if boxed (Box |
Never |
| Optimization | Inlined aggressively | Inlined aggressively |
When Closures May Be Less Efficient
Heap-Allocated Trait Objects (Box)
Using dynamic dispatch (e.g., Box
- Vtable Lookups: Indirect calls via function pointers.
- Cache Misses: Fat pointers (data + vtable) reduce locality.
let closures: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|x| x + 1),
Box::new(|x| x * 2),
]; // Heap-allocated, slower to call
Large Captured Environments
Closures storing large structs (e.g., 1KB buffer) increase memory usage and may inhibit inlining:
let data = [0u8; 1024]; // 1KB array
let closure = move || data.len(); // Closure size = 1KB + overhead
A closure is just an anonymous struct holding its captures, so "how expensive is it" is really a question about that struct's layout:
Excessive Monomorphization
Generic closures with many instantiations (e.g., in a hot loop) can bloat binary size:
(0..1_000).for_each(|i| { /* Unique closure per iteration */ });
Zero-Cost Abstractions in Practice
Static Dispatch (impl Fn)
Closures are as fast as regular functions when:
- Captured data is small (e.g., primitives).
- Monomorphization doesn't cause code bloat.
let add = |x, y| x + y; // Same ASM as `fn add(x: i32, y: i32) -> i32`
Example: Inlining
fn main() {
let x = 5;
let closure = || x * 2; // Inlined → no function call
println!("{}", closure()); // ASM: `mov eax, 10`
}
What the overhead actually is
✅ Use impl Fn for zero-cost static dispatch.
🚫 Avoid Box
Real-World Impact
- rayon uses closures with static dispatch for parallel iterators (no overhead).
- GUI frameworks like iced leverage closures for event handlers efficiently.
If you want to see the zero-cost claim rather than take my word for it:
cargo rustc -- --emit asm and diff the closure against the equivalent function.
Read next
- Step 47 of 58•6 minHow Rust's Ownership and Borrowing Ensure Safe Concurrency
Ownership and borrowing rule out data races at compile time. What Send and Sync add, and why Rc can't cross a thread boundary.
- Step 48 of 58•7 minDeadlocks in C vs Rust: What Does Rust Really Prevent?
- Step 49 of 58•6 minVec::push() in a loop vs. pre-allocating with Vec::with_capacity()?