Rust Performance Engineering

Zero-Cost Abstractions

A zero-cost abstraction is a high-level language feature that obeys two principles first articulated by Bjarne Stroustrup and central to Rust's design:

  1. What you don't use, you don't pay for.
  2. What you do use, you couldn't hand-code any better.

In Rust, generic functions, trait bounds, iterator pipelines (iter().zip().map().sum()), newtype wrappers, and zero-sized type states compile down to the exact same machine instructions as hand-written, low-level imperative loops.

High-Level Expressive Rust:
  `a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()`
                     |
                     v  Monomorphization + Inlining + SROA
Low-Level Machine Code:
  Identical (or faster due to eliminated bounds checks!) to a raw `while` index loop!

Primary Zero-Cost Abstractions in Rust

Abstraction Compile-Time Mechanism Runtime Overhead
Generics (fn foo<T: Trait>(x: T)) Monomorphized into specialized functions per concrete type T. 0 bytes / 0 cycles (direct static call, often inlined).
Iterator Adapters (map, filter, zip, fold) Nested iterator structs on the stack flattened by SROA and inlining into a single loop. 0 heap allocations; often faster than for i in 0..n because slice iterators avoid per-index bounds checks!
Newtypes (struct UserId(u64)) Type-checked at compile time; erased to raw u64 in MIR/LLVM IR. 0 bytes / 0 cycles (passed in the exact same CPU register as u64).
Option<&T> / Option<NonNull<T>> / Option<Box<T>> Null-pointer niche optimization (None stored as 0x0). 0 extra bytes (size_of::<Option<&T>>() == size_of::<&T>()).
Lifetimes ('a) and PhantomData<T> Verified by the borrow checker in MIR and erased completely before codegen. 0 bytes / 0 instructions (do not exist at runtime).

Demonstrating Zero-Cost Newtypes, Niche Options, and Iterator Fusion

use std::mem::size_of;
use std::num::NonZeroU64;

/// A strongly-typed distance in meters that prevents mixing up meters and feet at compile time,
/// while occupying the exact same 8-byte register as `f64` at runtime.
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Meters(pub f64);

/// High-level functional iterator pipeline:
pub fn weighted_sum_iter(values: &[u64], weights: &[u64]) -> u64 {
    values
        .iter()
        .zip(weights.iter())
        .map(|(&v, &w)| v.wrapping_mul(w))
        .fold(0u64, u64::wrapping_add)
}

/// Low-level imperative index loop:
pub fn weighted_sum_loop(values: &[u64], weights: &[u64]) -> u64 {
    let n = values.len().min(weights.len());
    let mut acc = 0u64;
    let mut i = 0;
    while i < n {
        acc = acc.wrapping_add(values[i].wrapping_mul(weights[i]));
        i += 1;
    }
    acc
}

fn main() {
    // 1. Verify zero size overhead of Newtypes and Niche-Optimized Options:
    assert_eq!(size_of::<Meters>(), size_of::<f64>());
    assert_eq!(size_of::<Option<NonZeroU64>>(), size_of::<u64>());
    assert_eq!(size_of::<Option<&Meters>>(), size_of::<*const Meters>());

    // 2. Verify high-level iterator pipeline produces identical results to manual loop:
    let vals = [10u64, 20, 30, 40];
    let wgts = [1u64, 2, 3, 4];

    assert_eq!(
        weighted_sum_iter(&vals, &wgts),
        weighted_sum_loop(&vals, &wgts)
    );
    println!("Weighted sum: {}", weighted_sum_iter(&vals, &wgts));
}
Weighted sum: 300

When an Abstraction Is Not Zero-Cost

Not every convenient feature in Rust is zero-cost; knowing the boundary between zero-cost static abstractions and runtime-cost abstractions prevents accidental bottlenecks:

  • Box<dyn Trait> / &dyn Trait: Carries a vtable pointer indirection (and heap allocation for Box), which can inhibit inlining and loop auto-vectorization inside tight inner loops.
  • Rc<RefCell<T>> / Arc<Mutex<T>>: Performs runtime reference-count updates and borrow-flag or atomic lock checks.
  • iter().collect::<Vec<_>>() in the middle of a pipeline: Allocates a temporary heap buffer. Keep iterator chains lazy until the final consumer!