Rust Performance Engineering

Profiling Applications

While microbenchmarks measure isolated functions in synthetic loops, application profiling observes an entire program running under a realistic workload to discover which functions, system calls, lock contentions, or I/O waits actually consume the most wall-clock or CPU time.

Application Profiling Workflow:

  1. Build with Release Optimizations + Symbol Line Tables + Frame Pointers
     (`--profile profiling` + `-C force-frame-pointers=yes`)
                           |
                           v
  2. Run Under Sampling or Tracing Profiler (`samply`, `cargo-flamegraph`, `perf`)
                           |
                           v
  3. Inspect Flamegraph / Call Tree (Identify widest horizontal bars!)

Preparing a Rust Binary for Accurate Profiling

By default, cargo build --release strips debug line tables (debug = false) and permits the compiler to use the frame pointer register (rbp / x29) as a general-purpose register—which can cause low-overhead kernel stack samplers (perf, DTrace, Instruments, samply) to produce truncated or [unknown] stack frames!

Configure a dedicated [profile.profiling] in Cargo.toml and enable frame pointers via .cargo/config.toml or RUSTFLAGS:

# File: Cargo.toml
[profile.profiling]
inherits = "release"
debug = "line-tables-only"   # Or `debug = true` for full inline stack frame expansion!
strip = "none"               # Keep function symbol names intact for the profiler
# Build with frame pointers enabled for accurate kernel stack-walking:
RUSTFLAGS="-C force-frame-pointers=yes" cargo build --profile profiling

Comparing Application Profiling Methodologies

Profiling Methodology Representative Tools Overhead What It Reveals Best
Statistical CPU Sampling samply (macOS/Linux/Win), cargo-flamegraph, Linux perf, macOS Instruments Very Low (~1–3%) Which functions and inlined loops burn the most on-CPU cycles.
Hardware Performance Counters (PMU) perf stat, Apple Instruments CPU Counters, Intel VTune Near Zero CPU instructions per cycle (IPC), branch mispredictions, and L1/L2/L3 cache misses.
Deterministic Instruction Counting Valgrind Callgrind / Cachegrind (iai-callgrind) High (10x–30x slower) Exact instruction and cache-miss counts with 0% OS jitter (ideal for CI diffing).
Structured Span Tracing (On + Off CPU) tracing crate + tracing-chrome / tokio-console Low (at coarse granularity) Off-CPU time: lock contention, async task scheduling delays, and network/disk I/O latency!

Reading a Flamegraph Correctly

In a Flamegraph (generated by cargo flamegraph or viewed interactively in Firefox Profiler via samply record ./target/profiling/my_app):

  • The Y-axis (Vertical) shows stack depth (parent functions at the bottom calling child functions above them).
  • The X-axis (Horizontal) shows the percentage of total sampled time (sorted alphabetically—not chronologically!).
  • What to look for: Look for "plateaus" (wide flat bars at the top of a stack tower). A wide bar at the very top means the CPU spent a large fraction of total runtime executing instructions directly inside that function!
use std::time::{Duration, Instant};

/// A lightweight scoped timer for coarse-grained phase profiling inside an application.
pub struct PhaseTimer {
    phase: &'static str,
    start: Instant,
}

impl PhaseTimer {
    pub fn start(phase: &'static str) -> Self {
        Self {
            phase,
            start: Instant::now(),
        }
    }

    pub fn finish(self) -> Duration {
        self.start.elapsed()
    }
}

fn main() {
    let timer = PhaseTimer::start("parse_and_index");
    let sum: u64 = (0..10_000u64).map(|x| x.wrapping_mul(3)).sum();
    let elapsed = timer.finish();

    println!(
        "Phase 'parse_and_index' produced sum={sum} in {:?} (elapsed >= 0: {})",
        elapsed,
        elapsed >= Duration::ZERO
    );
}
Phase 'parse_and_index' produced sum=149985000 in 18.5µs (elapsed >= 0: true)