- Inefficient Allocations
- Duplicate Operations
- Algorithmic Complexity
- Excessive Copying
- Blocking Operations
Problem:
// SLOW - millions of allocations
fn process_items(items: &[Item]) -> Vec<String> {
let mut result = Vec::new(); // Initial capacity = 0
for item in items {
result.push(item.to_string()); // May reallocate each time!
}
result
}How to find:
rg "Vec::new\(\)" --type rust # Find Vec::new() without with_capacityCorrect:
// FAST - single allocation
fn process_items(items: &[Item]) -> Vec<String> {
let mut result = Vec::with_capacity(items.len()); // Allocate memory upfront
for item in items {
result.push(item.to_string()); // Never reallocates
}
result
}
// EVEN FASTER - use iterators
fn process_items_fast(items: &[Item]) -> Vec<String> {
items.iter().map(|item| item.to_string()).collect()
// collect() is smart - allocates correct size automatically
}Measurable effect:
- For 10,000 elements: 2-3x faster
- For 100,000 elements: 5-10x faster
Problem:
// SLOW - creates new Strings
fn build_url(host: &str, path: &str, query: &str) -> String {
let mut url = String::new();
url.push_str("https://");
url.push_str(host);
url.push('/');
url.push_str(path);
url.push('?');
url.push_str(query);
url
}Correct:
// FAST - single allocation of correct size
fn build_url(host: &str, path: &str, query: &str) -> String {
let capacity = 8 + host.len() + 1 + path.len() + 1 + query.len();
let mut url = String::with_capacity(capacity);
url.push_str("https://");
url.push_str(host);
url.push('/');
url.push_str(path);
url.push('?');
url.push_str(query);
url
}
// OR use format! for short strings
fn build_url_simple(host: &str, path: &str, query: &str) -> String {
format!("https://{}/{}?{}", host, path, query)
}Problem:
// SLOW - creates intermediate Vec
fn sum_even_squares(nums: &[i32]) -> i32 {
let evens: Vec<_> = nums.iter().filter(|&n| n % 2 == 0).collect(); // Allocation!
let squares: Vec<_> = evens.iter().map(|&n| n * n).collect(); // Another one!
squares.iter().sum()
}Correct:
// FAST - zero allocations
fn sum_even_squares(nums: &[i32]) -> i32 {
nums.iter()
.filter(|&n| n % 2 == 0)
.map(|&n| n * n)
.sum()
// No intermediate Vec - all in single pass!
}Effect:
- For 1,000,000 elements: 10-20x faster
- 0 allocations vs 2 large allocations
Problem:
// SLOW - parsing initData TWICE
pub fn validate_telegram_data(raw: &str, hash: &str, bot_token: &str) -> bool {
let parsed = parse_query_string_raw(raw); // 1st time
validate_auth_date(&parsed);
let computed_hash = compute_hash(raw, bot_token); // Parses again inside!
// ^^^
}
fn compute_hash(raw: &str, bot_token: &str) -> String {
let parsed = parse_query_string_raw(raw); // 2nd time
// ...
}Solution:
// FAST - parse once
pub fn validate_telegram_data(raw: &str, hash: &str, bot_token: &str) -> bool {
let parsed = parse_query_string_raw(raw); // Once!
validate_auth_date(&parsed);
let computed_hash = compute_hash(&parsed, bot_token); // Pass reference
// ^^^^^^
}
fn compute_hash(parsed: &BTreeMap<String, String>, bot_token: &str) -> String {
// Use already parsed data
}Effect:
- -50% time on parsing
- Especially important for hot paths (called frequently)
1. Search for identical function calls:
# Find functions called > 1 time in same function
rg "(\w+)\(.*\).*\1\(" --type rust2. Visual search:
- Look for identical computations inside loops
- Check if expensive functions are called multiple times with same arguments
Example:
// BAD - computes len() on each iteration
for i in 0..items.len() {
if i < items.len() / 2 { // len() called again!
// ...
}
}
// GOOD
let len = items.len();
let half = len / 2;
for i in 0..len {
if i < half {
// ...
}
}Problem:
// O(n^2) - for each element traverse entire Vec
fn remove_duplicates(items: Vec<String>) -> Vec<String> {
let mut result = Vec::new();
for item in items {
if !result.contains(&item) { // O(n) for each element!
result.push(item);
}
}
result
}Correct:
use std::collections::HashSet;
// O(n) - use HashSet
fn remove_duplicates(items: Vec<String>) -> Vec<String> {
let mut seen = HashSet::new();
let mut result = Vec::new();
for item in items {
if seen.insert(item.clone()) { // O(1) on average
result.push(item);
}
}
result
}
// EVEN SIMPLER
fn remove_duplicates_simple(items: Vec<String>) -> Vec<String> {
items.into_iter().collect::<HashSet<_>>().into_iter().collect()
}Effect:
- For 1,000 elements: 100x faster
- For 10,000 elements: 1000x faster
Problem:
// O(n) for each search
fn find_user_by_id(users: &[User], id: i64) -> Option<&User> {
users.iter().find(|u| u.id == id) // Traverses entire array!
}
// In a loop - disaster:
for id in user_ids {
let user = find_user_by_id(&all_users, id); // O(n) each time!
}
// Total: O(m * n) where m = user_ids.len(), n = all_users.len()Correct:
use std::collections::HashMap;
// O(1) for each search
fn build_user_map(users: Vec<User>) -> HashMap<i64, User> {
users.into_iter().map(|u| (u.id, u)).collect()
}
let user_map = build_user_map(all_users); // O(n) once
for id in user_ids {
let user = user_map.get(&id); // O(1) each time!
}
// Total: O(n + m) instead of O(m * n)Effect for 10,000 searches in array of 10,000 elements:
- Was: ~50,000,000 operations (O(m*n))
- Now: ~20,000 operations (O(n+m))
- 2500x faster!
1. Nested loops:
rg "for.*\{[\s\S]{1,200}for" --type rust # Find nested loops2. Vec::contains in loop:
rg "\.contains\(" --type rust3. Linear search patterns:
rg "\.iter\(\)\.find|\.iter\(\)\.position" --type rustQuestions to ask yourself:
- How many times does the innermost operation execute?
- If data grows 10x, how much slower will it be?
- Can I use HashMap/HashSet/BTreeMap?
Problem:
// SLOW - copies entire Vec
fn process_items(items: Vec<String>) -> usize {
let copy = items.clone(); // Why?
copy.len()
}How to find:
rg "\.clone\(\)" --type rust # Check each clone!Correct:
// FAST - use reference
fn process_items(items: &[String]) -> usize {
items.len()
}Problem:
// SLOW - millions of allocations
fn log_items(items: &[Item]) {
for item in items {
tracing::debug!("Item: {}", item.id.to_string()); // Unnecessary to_string()
}
}Correct:
// FAST - Display formats itself
fn log_items(items: &[Item]) {
for item in items {
tracing::debug!("Item: {}", item.id); // Without to_string()
}
}Example from our project:
// WAS - copies Strings
let mut kv_pairs: Vec<(String, String)> = parsed.into_iter()...
// NOW - zero-copy
let mut kv_pairs: Vec<(&String, &String)> = parsed.iter()...Effect:
- For 100 key-value pairs of 20 characters each: ~4KB vs ~0 bytes allocations
- 3-5x faster
Problem:
// BLOCKS entire runtime!
async fn handle_request() -> Result<String> {
let data = std::fs::read_to_string("file.txt")?; // Sync I/O!
Ok(data)
}Correct:
// Async read
async fn handle_request() -> Result<String> {
let data = tokio::fs::read_to_string("file.txt").await?;
Ok(data)
}
// Or move to blocking pool
async fn handle_request_alt() -> Result<String> {
tokio::task::spawn_blocking(|| {
std::fs::read_to_string("file.txt")
}).await?
}Problem:
// BLOCKS other tasks
async fn expensive_computation(n: u64) -> u64 {
(0..n).sum() // Can run milliseconds/seconds
}Correct:
// In separate thread
async fn expensive_computation(n: u64) -> u64 {
tokio::task::spawn_blocking(move || {
(0..n).sum()
}).await.unwrap()
}cargo install flamegraph
cargo flamegraph --bin your-app
# Opens flamegraph.svg - visualization of time spent# Add to Cargo.toml:
# [dev-dependencies]
# criterion = "0.5"
# benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn benchmark_parse(c: &mut Criterion) {
let data = "auth_date=123&user=test&hash=abc";
c.bench_function("parse_query_string", |b| {
b.iter(|| parse_query_string_raw(black_box(data)))
});
}
criterion_group!(benches, benchmark_parse);
criterion_main!(benches);cargo benchcargo install cargo-asm
cargo asm your_crate::function_name- Vec::new() replaced with Vec::with_capacity() where size is known?
- No unnecessary .clone() and .to_string()?
- Iterators used instead of intermediate Vec?
- String::with_capacity() for concatenation?
- No O(n^2) where O(n) is possible?
- HashMap instead of linear search?
- No nested loops over large data?
- Correct data structures chosen?
- References (&T) used instead of owned (T)?
- No unnecessary .clone()?
- .to_owned()/.to_string() avoided in hot paths?
- No blocking operations in async?
- Long computations in spawn_blocking?
- Async I/O used?
- No duplicate computations?
- Profiling showed problems?
- Benchmarks pass within norms?
- Profiling showed a problem (don't optimize blindly!)
- It's a hot path (called frequently)
- Measurable effect (minimum 10% improvement)
- Cold paths (startup, rare operations)
- Code that's already fast enough
- If it worsens readability without real gain
Rule: "Premature optimization is the root of all evil" - Donald Knuth
But: "Premature pessimization is also evil" - choose correct data structures from the start.