Course contentsShow
← Rust
Lesson 1450 of 1,91012. Unsafe Rust and Low-Level AccessFree lesson

Minimizing Indirect Function Calls

You will optimize virtual calls and function pointers to enable better compiler inlining.

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.