What is Static Dispatch & Dynamic Dispatch?

Static Dispatch:

Static and Dynamic Dispatch in Rust : static dispatch is all about predictability and efficiency. When you use static dispatch, everything is resolved at compile time. It’s like having a well-organized blueprint. Here’s how it works:

  1. The Rust compiler knows the exact types involved in a function call or method invocation because you’ve defined them explicitly.
  2. Using this compile-time knowledge, the compiler selects the specific function or method to be called.
  3. It generates optimized machine code that directly invokes that function, avoiding any runtime decision-making or overhead.

Static dispatch is highly efficient because it eliminates any uncertainty at runtime. It’s ideal for scenarios where performance is crucial, and you want to ensure that the code behaves predictably.

For example, if you have a function that performs operations on integers and another for floats, using static dispatch, the compiler will decide which one to use based on the type information provided during compilation.

Dynamic Dispatch:

Dynamic dispatch introduces flexibility at runtime. Instead of determining which function to call during compilation, this decision is made while the program is running. Here’s how it works:

  1. At runtime, when you make a function call or method invocation, Rust’s runtime system looks up the appropriate function or method based on the actual type of the object.
  2. It then executes the selected function or method.
  3. This runtime lookup introduces some performance overhead because the program has to figure out which function to call each time that code is executed.

Dynamic dispatch is valuable when you need polymorphism or when you don’t know the exact type of an object until runtime. For instance, when you define traits or interfaces in Rust, you often employ dynamic dispatch to allow different types to implement those traits.

Summary

Static dispatch is like having a well-planned blueprint that leads to efficient and predictable code. Dynamic dispatch offers flexibility at the expense of some runtime overhead, making it suitable for situations where you require adaptability or when you’re dealing with different types implementing the same behaviour. Rust lets you choose between these approaches to best suit your specific needs. If you want a quick way to see what the Assembly code will look like you can use : https://godbolt.org/

Note: It’s more common to pass trait objects by reference for dynamic dispatch, but you can pass them by value when you want to take ownership of the object.

  • With static dispatch (using generic type parameters and trait bounds), you can pass values or references, depending on your design and requirements.
  • With dynamic dispatch (using trait objects), you typically pass trait objects by reference for runtime polymorphism.

Code:

trait Shape {
    fn area(&self)->f64;
}

// rectangle
struct Rectangle {
    l: f64,
    w: f64,
}
impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.l * self.w
    }
}

// triangle
struct Triangle {
    a: f64,
    b: f64,
    c: f64,
}
impl Shape for Triangle {
    fn area(&self) ->f64{
        let s = self.a + self.b +self.c/2.0;
        s*(s-self.a)*(s-self.b)*(s-self.c).sqrt()
    }
}

struct Circle {
    r : f64
}
impl Shape for Circle{
    fn area(&self)->f64{
        3.14 * self.r * self.r
    }
}

// static dispatch
fn foo_sd<T:Shape>(thing : T){
    let area = thing.area();
    println!("area = {}", area);
}

// dynamc dispatch 
fn foo_dd(t: &dyn Shape){
    let area = t.area();
    println!("area = {}", area);
}

fn main() {
    let r1 = Rectangle { l : 10.0, w: 20.0 };
    foo_sd(r1);
}
// static dispatch
fn foo_sd<T:Shape>(thing : T){
    let area = thing.area();
    println!("area = {}", area);
}

// dynamc dispatch 
fn foo_dd(t: &dyn Shape){
    let area = t.area();
    println!("area = {}", area);
}
std::f64::<impl f64>::sqrt:
        sqrtsd  xmm0, xmm0
        movsd   qword ptr [rsp - 8], xmm0
        movsd   xmm0, qword ptr [rsp - 8]
        ret

<example::Rectangle as example::Shape>::area:
        movsd   xmm0, qword ptr [rdi]
        mulsd   xmm0, qword ptr [rdi + 8]
        ret

.LCPI2_0:
        .quad   0x4000000000000000
<example::Triangle as example::Shape>::area:
        push    rax
        movsd   xmm0, qword ptr [rdi]
        addsd   xmm0, qword ptr [rdi + 8]
        movsd   xmm1, qword ptr [rdi + 16]
        movsd   xmm2, qword ptr [rip + .LCPI2_0]
        divsd   xmm1, xmm2
        addsd   xmm0, xmm1
        movaps  xmm2, xmm0
        subsd   xmm2, qword ptr [rdi]
        movaps  xmm1, xmm0
        mulsd   xmm1, xmm2
        movaps  xmm2, xmm0
        subsd   xmm2, qword ptr [rdi + 8]
        mulsd   xmm1, xmm2
        movsd   qword ptr [rsp], xmm1
        subsd   xmm0, qword ptr [rdi + 16]
        call    std::f64::<impl f64>::sqrt
        movaps  xmm1, xmm0
        movsd   xmm0, qword ptr [rsp]
        mulsd   xmm0, xmm1
        pop     rax
        ret

.LCPI3_0:
        .quad   0x40091eb851eb851f
<example::Circle as example::Shape>::area:
        movsd   xmm0, qword ptr [rip + .LCPI3_0]
        mulsd   xmm0, qword ptr [rdi]
        mulsd   xmm0, qword ptr [rdi]
        ret

https://www.eventhelix.com/rust/rust-to-assembly-static-vs-dynamic-dispatch/#:~:text=Rust%20supports%20two%20types%20of,not%20known%20at%20compile%20time.