Zero cost abstraction in Rust

Zero-cost abstraction is a fancy term that simply means that adding high-level constructs like a newtype, iterators, traits, or generics do not contribute to any runtime cost. Zero-cost abstractions don’t make anything faster, rather they make the runtime the same as if you wrote the lower-level unabstracted version (usually at the expense of compile time). You can write expressive, high-level code without incurring runtime overhead. This is fantastic because in most other programming languages abstractions are expensive.

Let’s see an example with traits:

trait Foo {
    fn foo(&self) -> u32;
}

struct Bar;

impl Foo for Bar {
    fn foo(&self) -> u32 {
        123
    }
}

fn main() {
    let bar = Bar;
    let foo = bar.foo();
    println!("{}", foo);
}

Here the Bar struct implements Foo the trait. But the Foo trait never exists in runtime. The foo method is implemented for Bar, so the compiler can generate efficient machine code for the call. Rust uses a technique called Static Dispatch and with that the compiler knows the concrete type at compile time and generates specialized code for each type implementing the trait, resulting in direct function calls. The trait we created is a zero-cost abstraction - it uses no more CPU, RAM, or code space to track the state of Foo, it has no actual representation in memory at runtime.

But the idea is not new. In fact, the zero-overhead principle is a C++ design principle that states:

  • You don’t pay for what you don’t use.
  • What you do use, you couldn’t hand code any better.

This is different from other popular programming languages like Go, Python, Ruby, Java etc. In Go, to handle goroutines all Go binaries come with a scheduler and a runtime for managing goroutines even if you never use it. In Rust, the async runtime is an optional package if you need the async features.

Let’s see another example with iterators. Here we sum the even numbers of an array:

fn sum_even_numbers1(numbers: &[i32]) -> i32 {
    numbers.iter().filter(|&&x| x % 2 == 0).sum()
}

fn sum_even_numbers2(numbers: &[i32]) -> i32 {
    let mut sum = 0;
    for &number in numbers {
        if number % 2 == 0 {
            sum += number;
        }
    }
    sum
}

The first function uses iterator and borrows functional programming concepts to compute the sum concisely. It’s also easier to understand. But even though we use an iterator and filter through it to allocate another array of even numbers to get its sum, the compiler generates the same assembly code as the second function.

Generics are also implemented without any runtime overhead in Rust with a technique called Monomorphization which we will discuss in a different article.

Takeaway

You don’t have to feel guilty about doing abstractions anymore in Rust. Don’t rush to optimize runtime performance, rather invest in abstractions, software design, and code readability.