Introduction to Rust

Zero-Cost Abstractions

Zero-cost abstractions are high-level programming constructs—such as generics, traits, closures, iterators, Option<T>, and newtype wrappers—that compile down to machine code at least as fast and compact as hand-written low-level code. Coined by Bjarne Stroustrup for C++, the principle has two parts: what you do not use, you do not pay for; and what you do use, you could not hand-code any better.

How Rust Achieves Zero Runtime Overhead

In many high-level languages, abstractions carry runtime costs: generic types box values on the heap, closures allocate environment objects, and iterator chains create intermediate arrays at each step. Rust eliminates these costs during compilation through four mechanisms:

Mechanism High-Level Abstraction What rustc and LLVM Generate
Monomorphization Generic functions (fn max<T: Ord>(a: T, b: T)) and structs Dedicated, type-specific machine functions with direct static calls and no boxing or vtable lookup
Inlining and Iterator Fusion Iterator chains (.iter().filter(...).map(...).sum()) A single tight CPU loop with closure bodies inlined directly into the loop body
Newtype and Wrapper Erasure Single-field structs (struct Meters(f64);) and references Identical register/stack layout to the inner primitive (f64); wrapper exists only during type checking
Niche Optimization Option<&T>, Option<Box<T>>, Option<NonZeroU32> Uses the invalid 0x0 null bit pattern for None, keeping size_of::<Option<&T>>() == size_of::<&T>()

Monomorphization of Generics

When you write a generic function or struct, rustc performs monomorphization: it stamps out a specialized copy of the function for each concrete type actually used in the program, and then optimizes each copy independently.

Generic Source Code:
  fn add_pair<T: std::ops::Add<Output = T>>(a: T, b: T) -> T { a + b }
  let x = add_pair(3_i32, 4_i32);
  let y = add_pair(1.5_f64, 2.5_f64);
                  |
                  v  (Monomorphization during compilation)
Specialized Machine Functions:
  fn add_pair_i32(a: i32, b: i32) -> i32 { a + b }   --> CPU `add w0, w0, w1`
  fn add_pair_f64(a: f64, b: f64) -> f64 { a + b }   --> CPU `fadd d0, d0, d1`
fn max_value<T: Ord>(a: T, b: T) -> T {
    if a >= b { a } else { b }
}

fn main() {
    let int_max = max_value(10_i32, 25_i32);
    let str_max = max_value("apple", "pear");

    println!("int_max = {int_max}, str_max = {str_max}");
}
int_max = 25, str_max = pear

If a generic type or method is never instantiated in your program, rustc generates zero machine instructions for it.

Iterator Chains vs Hand-Written Loops

Consider summing the squares of all even numbers in a slice. You can write this either as an imperative index loop or as a declarative chain of iterator adapters (filter, map, sum):

fn sum_even_squares_loop(values: &[u64]) -> u64 {
    let mut total = 0;
    for i in 0..values.len() {
        let v = values[i];
        if v % 2 == 0 {
            total += v * v;
        }
    }
    total
}

fn sum_even_squares_iter(values: &[u64]) -> u64 {
    values
        .iter()
        .copied()
        .filter(|&v| v % 2 == 0)
        .map(|v| v * v)
        .sum()
}

fn main() {
    let data = [1, 2, 3, 4, 5, 6];
    assert_eq!(sum_even_squares_loop(&data), sum_even_squares_iter(&data));
    println!("Sum of even squares: {}", sum_even_squares_iter(&data));
}
Sum of even squares: 56

In sum_even_squares_iter:

  1. .iter(), .copied(), .filter(...), and .map(...) are lazy—none of them allocate a temporary Vec or traverse the slice when called. Each adapter returns a small stack struct wrapping the previous iterator.
  2. When .sum() drives the iterator in an optimized build (-O), the compiler inlines the next() calls and closures, fusing the entire chain into a single loop that is identical to (or faster than, due to elided bounds checks and SIMD auto-vectorization) sum_even_squares_loop.

Zero-Cost Type Safety: The Newtype Pattern

Wrapping a primitive type in a single-field tuple struct (a newtype) lets the compiler prevent unit-mixing bugs (such as adding meters to seconds) with zero memory or CPU cost at runtime:

use std::mem::size_of;

#[derive(Debug, Clone, Copy)]
struct Meters(f64);

#[derive(Debug, Clone, Copy)]
struct Seconds(f64);

fn speed(distance: Meters, time: Seconds) -> f64 {
    distance.0 / time.0
}

fn main() {
    let d = Meters(100.0);
    let t = Seconds(9.58);

    println!("Speed: {:.2} m/s", speed(d, t));
    println!("size_of::<f64>()    = {} bytes", size_of::<f64>());
    println!("size_of::<Meters>() = {} bytes", size_of::<Meters>());
}
Speed: 10.44 m/s
size_of::<f64>()    = 8 bytes
size_of::<Meters>() = 8 bytes

Passing speed(Seconds(9.58), Meters(100.0)) is rejected at compile time (E0308: expected Meters, found Seconds), yet at runtime Meters(100.0) is passed in the exact same floating-point CPU register as a bare 100.0_f64.

Zero-Cost Null Safety with Niche Optimization

In C, a pointer const int* occupies 8 bytes (on 64-bit systems) and uses address 0x0 (NULL) to represent absence. In Rust, references (&T) and smart pointers (Box<T>, NonNull<T>) are guaranteed never to be null (0x0).

When you wrap a reference in Option<&T>, the compiler uses 0x0 to represent None and any non-zero address to represent Some(&T). As a result, Option<&T> has the exact same 8-byte size and machine representation as a nullable C pointer, while forcing you to check for None at compile time:

use std::mem::size_of;

fn main() {
    println!("size_of::<&i32>()         = {} bytes", size_of::<&i32>());
    println!("size_of::<Option<&i32>>() = {} bytes", size_of::<Option<&i32>>());
    println!("size_of::<Box<i32>>()         = {} bytes", size_of::<Box<i32>>());
    println!("size_of::<Option<Box<i32>>>() = {} bytes", size_of::<Option<Box<i32>>>());
}
size_of::<&i32>()         = 8 bytes
size_of::<Option<&i32>>() = 8 bytes
size_of::<Box<i32>>()         = 8 bytes
size_of::<Option<Box<i32>>>() = 8 bytes

Trade-Offs of Zero-Cost Abstractions

While zero-cost abstractions eliminate runtime execution overhead, they shift work to the compiler:

Dimension Benefit Trade-Off
Runtime speed Direct calls, aggressive inlining, SIMD vectorization None
Compilation time N/A Heavy generic instantiation and optimization increase compile times
Binary size Unused generic functions are stripped Monomorphizing large generic functions across many types can increase code size ("code bloat") compared to dynamic dispatch (dyn Trait)