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
57: Benchmarking Rust Code
I've spent a lot of my career guessing why a piece of code feels sluggish. I'll tell myself, "I bet this loop is the problem," rewrite it, and then feel reasonably confident it's faster. But "feeling" fast isn't engineering. In Rust, where we have so many choices—heap vs. stack, different collection types, various iterator patterns—guessing is a dangerous game.
The Quick and Dirty Way
When I first suspect a bottleneck, I usually start with something simple. Let's say I'm building a system that aggregates a massive list of IDs into a single comma-separated string. I'll write a quick function and wrap it in std::time::Instant to see what's happening.
use std::time::Instant;
fn naive_concat(items: &[&str]) -> String {
let mut result = String::new();
for item in items {
result.push_str(item);
result.push_str(",");
}
result
}
fn main() {
let data = vec!["id_123"; 10_000];
let start = Instant::now();
let _res = naive_concat(&data);
let duration = start.elapsed();
println!("Took: {:?}", duration);
}
I run this with cargo run and it takes, say, 15 milliseconds. Seems fine. But wait—I know that appending to a string repeatedly causes multiple re-allocations as the buffer grows. I suspect that if I pre-allocate the memory, it'll fly.
Wait, why is this so slow?
I rewrite it using String::with_capacity, run it again, and... it's almost exactly the same. I'm confused. I know the logic is more efficient. I check my terminal and realize I'm running in debug mode. This is the classic Rust trap.
In debug mode, Rust doesn't optimize your code. It keeps all the checks and doesn't inline functions. I run it again with cargo run --release. Suddenly, the with_capacity version is an order of magnitude faster. But here's the problem: Instant is a one-shot deal. If my OS decides to run a background update or a browser tab spikes in CPU usage during that one run, my numbers are garbage.
Moving to a Professional Rig
If I'm going to make a design decision based on performance, I need a statistical sample, not a single snapshot. This is where criterion comes in. It's the gold standard for benchmarking in Rust because it runs the function thousands of times, calculates the mean, and tells you if a change is actually a statistically significant improvement.
First, I have to tell Cargo that I'm adding a benchmark suite. I'll add this to my Cargo.toml:
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "concat_bench"
harness = false
The harness = false part is crucial; it tells Rust not to use the built-in test runner, because criterion provides its own.
Measuring the Win
Now I'll create a file at benches/concat_bench.rs. I want to compare my naive approach against the optimized one side-by-side. This is where the "exploration" becomes real data.
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn naive_concat(items: &[&str]) -> String {
let mut result = String::new();
for item in items {
result.push_str(item);
result.push_str(",");
}
result
}
fn optimized_concat(items: &[&str]) -> String {
// Pre-calculating capacity to avoid re-allocations
let total_size: usize = items.iter().map(|s| s.len() + 1).sum();
let mut result = String::with_capacity(total_size);
for item in items {
result.push_str(item);
result.push_str(",");
}
result
}
fn criterion_benchmark(c: &mut Criterion) {
let data = vec!["id_123"; 10_000];
c.bench_function("naive", |b| b.iter(|| naive_concat(black_box(&data))));
c.bench_function("optimized", |b| b.iter(|| optimized_concat(black_box(&data))));
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
Notice that black_box call? That's a little piece of magic. Without it, the Rust compiler is too smart. It might realize that the input data never changes and that the output is never used, and it might just optimize the entire function call away into a constant. black_box tells the compiler, "Don't try to be clever here; actually execute this code."
I run cargo bench, and criterion gives me a detailed breakdown. It tells me the optimized version is 3x faster and generates an HTML report with graphs. Now I'm not guessing; I have a proof. This process—identifying a suspicion, trying a quick Instant check, and then verifying with a rigorous criterion bench—is exactly how I handle performance tuning in production.
📋 Practical Task
Optimizing a Large Vector Filter
You have a function that filters a large vector of integers to find only the even numbers and collects them into a new vector. Your goal is to benchmark two different approaches and prove which one is faster.
The Setup:
- Create a new Rust project and add
criterionto yourdev-dependenciesinCargo.toml. - Configure the
[[bench]]section inCargo.tomlto point to a benchmark file namedfilter_bench.rs. - Implement two functions in
benches/filter_bench.rs:filter_naive: Use aforloop andpush()to move even numbers into a newVec.filter_iterator: Use.iter().filter().cloned().collect().
- Set up a
criterion_groupthat benchmarks both functions using a vector containing 100,000 random integers. - Run
cargo benchand identify which method performs better.
Requirement: Ensure you use black_box to prevent the compiler from optimizing away your filtering logic.
There are no comments for now.