logo

Rust in Depth - Chapter 2

Feb 11 · 10min

Monomorphization

Today, we are going to talk about monomorphization. It is an important concept in Rust, which is used to optimize the performance of the code.

Let’s show the example:

pub fn strlen(s: impl AsRef<str>) -> usize {
    s.as_ref().len()
}

fn main() {
    strlen("Hello");
    strlen(String::from("hello"));
}

In this example, strlen function have a parameter which implements the AsRef<str> trait. When main function calls it in two different ways, the Rust compiler will generate two different functions for each call. It is different from other languages, such as C/C++.

Monomorphization actually helps the Rust compiler to improve the performance of the code. But it have a problem in distributing the binary file. The binary file will be larger than other languages, and the most important is the compile function only can be generated when the function is called.

Static Dispatch

Static dispatch means Rust compiler can generate the code at compile time.

pub trait Hei{
    fn hei(&self);
}
impl Hei for &str{
  fn hei(&self){
    println!("Hei, {}", self);
  }
}
pub fn foo(h: impl Hei){
  h.hei();
}

In this example, the foo function have a parameter which implements the Hei trait. This is a generic function, and the Rust compiler will generate the code at compile time. Actually, the compiler do not know the type of h at compile time, and this is where monomorphization comes into play.

such as this:

// compiler generate
pub fn foo_str(h: &str){
  h.hei();
}

fn main(){
  foo("world");
}

So, this is Static Dispatch.

Dynamic Dispatch

Here we have an idea.

fn bar(s: &[dyn Hei]){
  for i in s{
    i.hei();
  }
}
fn main(){
  let v = vec!["world", "hello"];
  bar(&v);
  let error = vec![String::from("world"), "hello"]; // This will cause an error
}

Imagine that we have a multiple elements implements the Hei trait, and we want to use bar function to call the hei function. But the Rust compiler will cause an error, because the s parameter don’t have Sized.

Why? Static Dispatch don’t need to Sized the type of parameter, but Dynamic Dispatch need to Sized the type of parameter.

Here is an example i think it can well explain the difference between Static Dispatch and Dynamic Dispatch.

trait Plugin {
    fn execute(&self);
}

struct PluginA;
impl Plugin for PluginA {
    fn execute(&self) {}
}

struct PluginB;
impl Plugin for PluginB {
    fn execute(&self) {}
}

fn run_plugin(plugin: &dyn Plugin) {
    plugin.execute();
}

fn main() {
    let plugin_a = PluginA;
    let plugin_b = PluginB;

    run_plugin(&plugin_a);
    run_plugin(&plugin_b);
}

The Plugin System is a good example to show why we need Dynamic Dispatch. Because we actually don’t know the size of the plugin at compile time. And Rust compiler must know all the size of the type at compile time.

So, how to fix the error of the first example? Here are two ways:

// use `&`
fn bar(s: &[&dyn Hei]){
  for i in s{
    i.hei();
  }
}

// use `Box`
fn bar(s: &[Box<dyn Hei>]){
  for i in s{
    i.hei();
  }
}
fn main(){
let v = vec![String::from("world"), "hello"];
bar(&v);
}

Fat Pointer(VTables)

Next, I want to show you how the Rust compiler find the function of the trait.

pub trait Hei{
    fn hei(&self);
}
impl Hei for &str{
  fn hei(&self){
    println!("Hei, {}", self);
  }
}

fn main(){
  let s = "world";
  s.hei();
}

Here is an example. The Rust compiler will generate a Fat Pointer for the &str type. The Fat Pointer contains two parts: the first part is the pointer to the data, and the second part is the pointer to the VTable.

// compiler generate
// &str -> &dyn Hei
// 1. pointer to the &str
// 2. &HeiVtable{
//  hei: &<str as Hei>::hei
// }

fn main(){
  let s = "world";
  s.hei();
  // s.vtable.hei(s.pointer);
}

Multiple Traits

If we want to use multiple traits, did the Rust compiler generate multiple VTables?

The answer is Yes.

// ignore this bug
pub fn baz(s: &dyn Hei + dyn Hei2){
  s.hei();
  s.hei2();
}

The Rust compiler will generate two VTables for the s.

// compiler generate
// &str -> &dyn Hei + dyn Hei2
// 1. pointer to the &str
// 2. &HeiVtable{
//  hei: &<str as Hei>::hei
// }
// 3. &Hei2Vtable{
//  hei2: &<str as Hei2>::hei2
// }

And Rust give us a way to optimize the VTables which means make them into one VTable.

pub trait HeiAsRef: Hei + Hei2 {}
pub fn barz<T: HeiAsRef>(s: &T) {
  s.hei();
  s.hei2();
}
> share with
>
CC BY-NC-SA 4.0 2024-PRESENT © Chen Tao