Rust assembly generation: Static vs dynamic dispatch

Traits in Rust are similar to interfaces in other languages. Traits permit the implementation of a common interface for multiple types without the need to duplicate the code for each type. Developers can then write code in terms of the traits rather than the concrete types.

Rust supports two kinds of dispatch: static and dynamic.

We will use the following example to illustrate the difference between static and dynamic dispatch.

// The Shape trait requires that the implementor have a method called `area`
// that returns the area as the associated type `Shape::T`.
pub trait Shape {
    type T;
    fn area(&self) -> Self::T;
}

// A generic Point for a specified type T.
pub struct Point<T> {
    x: T,
    y: T,
}

// A generic Rectangle for a specified type T.
pub struct Rectangle<T> {
    top_left: Point<T>,
    bottom_right: Point<T>,
}

// Implement the Shape trait for the Rectangle using a generic type T.
// The `T` is the return type of the `area` method. The where clause specifies
// that `T` must support subtraction, multiplication and ability to copy.
impl<T> Shape for Rectangle<T>
where
    T: std::ops::Sub<Output = T> + std::ops::Mul<Output = T> + Copy,
{
    type T = T;
    fn area(&self) -> T {
        let width = self.bottom_right.x - self.top_left.x;
        let height = self.top_left.y - self.bottom_right.y;

        width * height
    }
}

// A function that calculates the area of two shapes and returns a tuple containing the areas.
// This function requires that the two shapes implement the Shape trait. The function will call
// the area function via a static dispatch. Code will be generated for the function only if concrete
// types are specified for `a` and `b`.
pub fn area_pair_static(a: impl Shape<T = f64>, b: impl Shape<T = f64>) -> (f64, f64) {
    (a.area(), b.area())
}

pub fn static_dispatch_pair(a: Rectangle<f64>, b: Rectangle<f64>) -> (f64, f64) {
    // The following line will generate code for the function as concrete types are specified for `a` and `b`.
    area_pair_static(a, b)
}

// This function performs the same function as `area_pair_static` but uses a dynamic dispatch. The compiler
// will generate code for this function. The calls to `area` are made through the `vtable`.
pub fn area_pair_dynamic(a: &dyn Shape<T = f64>, b: &dyn Shape<T = f64>) -> (f64, f64) {
    (a.area(), b.area())
}

// Using the dynamic dispatch. In this case, the compiler should generate a call to the `area_pair_dynamic` function.
// Spoiler alert: The compiler optimizes away the call to `area_pair_dynamic` if the concrete types are known. If converts
// the dynamic dispatch to a static dispatch.
pub fn dynamic_dispatch_pair(a: Rectangle<f64>, b: Rectangle<f64>) -> (f64, f64) {
    area_pair_dynamic(&a, &b)
}

dyn trait pointer

Static dispatch

pub fn area_pair_static(a: impl Shape<T = f64>, b: impl Shape<T = f64>) -> (f64, f64) {
    (a.area(), b.area())
}

pub fn static_dispatch_pair(a: Rectangle<f64>, b: Rectangle<f64>) -> (f64, f64) {
    area_pair_static(a, b)
}
; Input parameters:
; a: Rectangle<f64> in rdi
; b: Rectangle<f64> in rsi
; Output parameters:
; Tuple<f64, f64> in (xmm0, xmm1)
example::static_dispatch_pair:
        movsd   xmm0, qword ptr [rdi + 8] ; a.bottom_right.y
        movsd   xmm2, qword ptr [rdi + 16] ; a.top_left.x
        movsd   xmm1, qword ptr [rsi + 8] ; b.bottom_right.y
        movsd   xmm3, qword ptr [rsi + 16] ; b.top_left.x
        subsd   xmm2, qword ptr [rdi]; a.width = a.bottom_right.x - a.top_left.x
        subsd   xmm0, qword ptr [rdi + 24]; a.height = a.top_left.y - a.bottom_right.y
        subsd   xmm3, qword ptr [rsi] ; b.width = b.bottom_right.x - b.top_left.x
        mulsd   xmm0, xmm2; a.area = a.width * a.height
        subsd   xmm1, qword ptr [rsi + 24] ; b.bottom_right.y - b.top_left.y
        mulsd   xmm1, xmm3 ; b.width * b.height
        ret

Dynamic dispatch

pub fn area_pair_dynamic(a: &dyn Shape<T = f64>, b: &dyn Shape<T = f64>) -> (f64, f64) {
    (a.area(), b.area())
}
; Input parameters:
; rdi contains the address of the first object (a).
; rsi contains the address of the vtable of the first object (a)
; rbx contains the address of the second object (b).
; rcx contains the address of the vtable of the second object (b)
; Output parameters:
; Tuple<f64, f64> in (xmm0, xmm1)

example::area_pair_dynamic:
        push    r14                     ; Save previous r14
        push    rbx                     ; Save previous rbx
        push    rax                     ; Save previous rax
        mov     r14, rcx                ; r14 = address of the vtable of b
        mov     rbx, rdx                ; rbx = b (Get the address of b)

        ; Dynamic dispatch:
        ; rdi contains address of a
        ; rsi contains address of the vtable for a's type
        call    qword ptr [rsi + 24]    ; Obtain the address of the area function from the vtable and call it.
        movsd   qword ptr [rsp], xmm0   ; Save a.area() return value in a local variable

        mov     rdi, rbx                ; rdi = address of b

        ; Dynamic dispatch:
        ; rdi contains address of b
        ; r14 contains address of the vtable for b's type
        call    qword ptr [r14 + 24]    ; Obtain the address of the area function from the vtable and call it.
        movaps  xmm1, xmm0              ;Save b.area() return value in xmm1

        movsd   xmm0, qword ptr [rsp]   ;Get a.area() return value from a local variable
        add     rsp, 8                  ;Remove local variable from stack
        pop     rbx                     ;Restore previous rbx
        pop     r14                     ;Restore previous r14
        ret                             ;Return a.area() and b.area() as (xmm0, xmm1)

Dynamic dispatch optimized to a static dispatch

pub fn dynamic_dispatch_pair(a: Rectangle<f64>, b: Rectangle<f64>) -> (f64, f64) {
    area_pair_dynamic(&a, &b)
}
example::dynamic_dispatch_pair:
        movsd   xmm0, qword ptr [rdi + 8]   ; a.bottom_right.y
        movsd   xmm1, qword ptr [rdi + 16]  ; a.top_left.x
        subsd   xmm1, qword ptr [rdi]       ; a.width = a.bottom_right.x - a.top_left.x
        subsd   xmm0, qword ptr [rdi + 24]  ; a.height = a.top_left.y - a.bottom_right.y
        mulsd   xmm0, xmm1                  ; a.area = a.width * a.height
        movsd   xmm2, qword ptr [rsi + 16]  ; b.bottom_right.x
        subsd   xmm2, qword ptr [rsi]       ; b.width = b.bottom_right.x - b.top_left.x
        movsd   xmm1, qword ptr [rsi + 8]   ; b.bottom_right.y
        subsd   xmm1, qword ptr [rsi + 24]  ; b.height = b.top_left.y - b.bottom_right.y
        mulsd   xmm1, xmm2                  ; b.area = b.width * b.height
        ret

Reference