Rust / the practical course

04 / Systems practice

Measure performance and resource use

Measure speed and memory use before changing how the program works.

60 min + practiceRust 1.98.1 · Edition 2024

By Robert DeVore · Download Markdown

A measurement starts with a question

Does parsing allocate? Does input reading dominate runtime? Does parallelism help on representative files? Each question needs its own test. “Rust is fast” does not answer any of them.

Run the example

cargo run --locked --example 23_performance
use std::{hint::black_box, time::Instant};
fn main() {
    let input = "INFO small message";
    let start = Instant::now();
    for _ in 0..10_000 {
        black_box(audit_core::parse(black_box(input)).unwrap());
    }
    println!(
        "10,000 parses: {:?}; one sample, not a benchmark conclusion",
        start.elapsed()
    );
}

View the tested source

The example shows how to time code and use black_box. One timing result is not enough to draw a conclusion. Startup, optimization, CPU frequency, background work, and tiny sample size can dominate. black_box is a best-effort optimization barrier useful in experiments, not a mathematical guarantee about generated code.

Design the experiment

Run optimized builds when evaluating optimized production behavior. Record compiler version, target, profile, input distribution, hardware, and sample count. Make sure both versions produce the same output and handle errors the same way. Include empty input, typical records, long records near the limit, and malformed data if those occur in the workload.

Separate warm-up from samples where appropriate. Report a distribution rather than one favorable run. Change one significant variable at a time. If you claim lower memory use, measure it or prove a specific bound and state precisely which allocations the bound covers.

Our parser borrows message text rather than allocating a new string. That is visible in its interface and implementation. The reader keeps one bounded record vector, while the caller's buffering layer can have its own allocation. A borrowed design still pays for reading, validation, and counting; avoiding one allocation does not mean the whole program is zero-cost.

Optimize the bottleneck

A profile may show that filesystem I/O dominates, in which case replacing an iterator with a loop is unlikely to matter. It may show formatting costs, in which case current standard-library APIs deserve review before adding a crate. Rust 1.98 introduced integer buffer formatting APIs, but our three-line-sized summary does not justify optimizing that path without evidence.

Monomorphization can improve inlining and also increase compile time or code size. Dynamic dispatch has indirection costs and can reduce duplication. Arc::clone updates an atomic count; String::clone copies text. Treat these as different operations. Check whether unnecessary copies or allocations cause the slowdown before reaching for unsafe code.

Exercise

Compare parsing borrowed records with a version that creates a new owned message for each record. Preserve outputs and error handling. Record at least several repeated optimized runs and explain which part of the measurement is noisy. Do not commit a machine-specific speed claim as a universal fact.

Solution and acceptance check

Keep input bytes and loop counts identical, consume results so they cannot trivially disappear, and record toolchain/profile. The owned version allocates new message strings, but the timing difference may vary. A useful conclusion states the tested workload and uncertainty. If the difference is below noise, report that rather than selecting the fastest run.

Sources: black_box, Cargo profiles, and Rust 1.98 release notes.

Find a lesson