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:
- The Rust compiler knows the exact types involved in a function call or method invocation because you’ve defined them explicitly.
- Using this compile-time knowledge, the compiler selects the specific function or method to be called.
- 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:
- 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.
- It then executes the selected function or method.
- 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