Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
133: criterion for Benchmarking
We've all been there: you write a piece of logic, you have a hunch that a map is slower than a for loop in a specific context, or you've just swapped out a HashMap for a BTreeMap and you want to see if it actually mattered. Your first instinct is probably to reach for std::time::Instant, wrap your function in a loop, and print the elapsed time. It feels intuitive, and for a quick-and-dirty sanity check, it's fine. But if you're trying to make an engineering decision based on those numbers, you're walking into a trap.
The Stopwatch Fallacy
Let's look at the naive way. Imagine we're benchmarking a simple function that parses a string of comma-separated integers into a Vec<i32>. You might write something like this in your main.rs:
let data = "1,2,3,4,5,6,7,8,9,10".repeat(100);
let start = std::time::Instant::now();
for _ in 0..1000 {
let _res: Vec<i32> = data.split(',').map(|s| s.parse().unwrap()).collect();
}
let duration = start.elapsed();
println!("Took: {:?}", duration);
On the surface, this looks reasonable. But here is the problem: you aren't just measuring your code; you're measuring the noise of your entire operating system. A background process might wake up, the CPU might throttle its clock speed, or the OS might decide to move your thread to a different core mid-execution. One run says 12ms, the next says 15ms. You have no idea if that 3ms difference is your code change or just Windows Update deciding to check for patches in the background.
When the Compiler Cheats
There's a more insidious problem here: LLVM is too smart for its own good. In the example above, I assigned the result to _res, but I never actually used _res. If you compile this in --release mode, the compiler looks at that loop and realizes the result of the parsing is never read. It concludes that the entire operation has no side effects and simply deletes the whole loop. Your benchmark suddenly reports that the code took 2 microseconds, and you're left wondering why your parser is suddenly the fastest piece of software ever written.
You can try to fight this by marking the result as volatile or using some hacks, but that's a losing battle. This is why we move away from manual timing and toward a dedicated benchmarking framework. In the Rust ecosystem, that means criterion.
Statistical Rigor with Criterion
Criterion doesn't just run your code once; it runs it thousands of times, analyzes the distribution, and uses linear regression to filter out the noise. It handles the "warm-up" phase—where the CPU caches are populated and the branch predictor starts to learn—so you aren't penalizing your code for the first few cold iterations.
To use it, you don't put your benchmarks in src/. You create a benches/ directory in your project root. Here is how that same parsing benchmark looks using Criterion:
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn parse_csv(data: &str) -> Vec<i32> {
data.split(',').map(|s| s.parse().unwrap()).collect()
}
fn bench_parsing(c: &mut Criterion) {
let data = "1,2,3,4,5,6,7,8,9,10".repeat(100);
c.bench_function("csv_parse", |b| b.iter(|| {
// We wrap the input in black_box to prevent the compiler
// from optimizing the call away or pre-calculating the result.
parse_csv(black_box(&data))
}));
}
criterion_group!(benches, bench_parsing);
criterion_main!(benches);
The black_box function is the secret sauce here. It tells the compiler: "I know you think you know what's happening here, but pretend you have no idea what this value is." This forces the compiler to actually execute the function every single time, preventing the "cheating" we saw earlier.
The Cost of Precision
Is Criterion overkill? Sometimes. It adds a dependency to your dev-dependencies, it requires a specific project structure, and it takes significantly longer to run because it's doing actual statistics. If you just want to know if a function takes 1 second or 10 seconds, Instant::now() is fine. But if you're trying to decide between two different algorithms where the difference is measured in nanoseconds, you cannot trust anything other than a statistically sound benchmark. I've spent hours chasing "performance regressions" that turned out to be nothing more than a Chrome tab eating CPU cycles in the background—don't make that mistake.
📋 Practical Task
Benchmarking String Concatenation Strategies
In this exercise, you will compare two ways of building a large string: using the format! macro in a loop versus using a String buffer with push_str. Your goal is to prove which one is faster using criterion.
- Add
criterionto your[dev-dependencies]inCargo.tomland add the[[bench]]target configuration. - Create a file at
benches/concat_bench.rs. - Implement two functions:
bench_format(which usesformat!("{}{}", acc, item)in a loop) andbench_push(which usesacc.push_str(item)). - Wrap your inputs in
black_boxto ensure the compiler doesn't optimize the loops away. - Run
cargo benchand observe the results. - Requirement: Identify the exact time difference per iteration and write a brief comment in your code explaining why the
push_strapproach is more efficient in terms of memory allocation.
There are no comments for now.