Sling Academy
Home/Rust/Rust - Distinguishing static dispatch vs dynamic dispatch in generic code

Rust - Distinguishing static dispatch vs dynamic dispatch in generic code

Last updated: January 07, 2025

When programming in Rust, understanding the nuances of static dispatch and dynamic dispatch is crucial for writing efficient and effective code, especially when dealing with generics. These dispatch mechanisms determine how methods are called, and the cost associated with calling them, either at compile-time or runtime, which can significantly impact performance and flexibility.

Understanding Static Dispatch

Static dispatch in Rust involves resolving method calls at compile-time. This is typically seen with generics where the compiler inlines the function implementations for each concrete type it encounters. For instance:

fn static_dispatch_example(item: T) {
    item.do_something();
}

In the example above, the compiler generates specific implementations of the static_dispatch_example function for each type passed to it that implements the MyTrait. This process is known as monomorphization, an approach where different versions of the function are created for each actual type used as a parameter. Here are some benefits:

  • Performance: Fetching the function is faster since it directly jumps to the resolved function pointer.
  • Optimization: Inline expansion of functions can lead to more opportunities for optimizations like dead code elimination.

Downsides of Static Dispatch

While static dispatch is beneficial for performance, it may increase the binary size because each instantiation of a function for different types results in a separate copy of that function.

Exploring Dynamic Dispatch

Dynamic dispatch, on the other hand, happens at runtime, and it typically involves the use of trait objects. It allows you to handle types based on shared behavior defined by trait rather than the actual data type. Here's how it is implemented:

fn dynamic_dispatch_example(item: &dyn MyTrait) {
    item.do_something();
}

Using dynamic dispatch involves the concept of a vtable (virtual method table), where a lookup table at runtime determines which method to call. Here are some key advantages:

  • Flexibility: You can store different types in the same collection or pass as arguments as long as they implement the same trait.
  • Decoupling: Code can be written without being concerned about all possible types implementing a given trait at compile time.

Drawbacks of Dynamic Dispatch

Despite its flexibility, dynamic dispatch carries some overhead:

  • Function calls require an extra indirection, which might be slower than static dispatch.
  • It might inhibit certain compiler optimizations, potentially leading to less efficient code.

Choosing Between Static and Dynamic Dispatch

The decision to use static or dynamic dispatch largely depends on your specific use case:

  • If runtime performance and minimizing binary size are crucial, lean towards static dispatch.
  • If flexibility is essential, or if the types involved are not known until runtime, then dynamic dispatch is more suitable.

Static vs Dynamic Dispatch Example

This example shows both approaches with the same trait:

trait MyTrait {
    fn do_something(&self);
}

struct TypeA;
struct TypeB;

impl MyTrait for TypeA {
    fn do_something(&self) {
        println!("TypeA doing something");
    }
}

impl MyTrait for TypeB {
    fn do_something(&self) {
        println!("TypeB doing something");
    }
}

fn static_example(item: T) {
    item.do_something();
}

fn dynamic_example(item: &dyn MyTrait) {
    item.do_something();
}

fn main() {
    let a = TypeA;
    let b = TypeB;

    static_example(a);
    // static_example(b); Not possible unless re-instantiated or using it directly with its type

    let a: &dyn MyTrait = &TypeA;
    let b: &dyn MyTrait = &TypeB;

    dynamic_example(a);
    dynamic_example(b);
}

In this code:

  • The static_example function uses static dispatch resulting in separate inlined instances for TypeA and TypeB.
  • The dynamic_example function uses dynamic dispatch, allowing both TypeA and TypeB to be passed as trait objects.

Conclusion

Understanding the difference between static and dynamic dispatch in Rust is essential for optimizing both performance and flexibility in your code. While static dispatch can lead to better-optimized binaries with less runtime overhead, dynamic dispatch allows for greater flexibility in your programming model when type diversity is crucial.

Next Article: Rust - Creating your own traits to define behavior for generic types

Previous Article: Rust - Understanding how monomorphization works under the hood with generics

Series: Generic types in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior