Systematic optimization hunter for lading. Finds memory optimizations AND bugs - both are valuable. Run /lading-optimize-validate when bugs are discovered.
This document explains the reasoning behind each optimization pattern in the lading-optimize-hunt skill. These patterns are based on well-established Rust performance best practices and have been validated through benchmarks and community research.
Vec::with_capacity(n) / String::with_capacity(n) / FxHashMap::with_capacity(n)Pattern: Vec::new() + repeated push operations
Why it matters: When you know (or can estimate) the final size of a collection, preallocating avoids multiple reallocations as the collection grows.
How it works:
Vec starts with capacity 0 and doubles when full (0 → 4 → 8 → 16 → 32...)Example:
// Slow: multiple reallocations
let mut v = Vec::new();
for i in 0..1000 {
v.push(i); // May trigger reallocation
}
// Fast: single allocation
let mut v = Vec::with_capacity(1000);
for i in 0..1000 {
v.push(i); // Never reallocates
}
Bug risk: None - purely additive optimization
References:
write!() to reused buffer instead of format!()Pattern: Building strings in loops using format!() or repeated format!() + push_str()
Why it matters: format!() always allocates a new String, even if you immediately append it to another String. Using write!() to a reused buffer eliminates intermediate allocations.
Performance impact:
format!() to write!()How it works:
// Slow: allocates a new String for each format!()
let mut result = String::new();
for i in 0..1000 {
let formatted = format!("Item {}: {}", i, value); // Allocates!
result.push_str(&formatted);
}
// Fast: writes directly to the buffer
use std::fmt::Write;
let mut result = String::with_capacity(estimated_size);
for i in 0..1000 {
write!(result, "Item {}: {}", i, value).unwrap(); // No allocation
}
Additional benefits:
Bug risk: Format errors (usually caught at compile time), capacity estimation errors (causes reallocation but still correct)
References:
&[T] instead of &Vec<T>, &str instead of &StringPattern: Function parameters using &Vec<T> or &String
Why it matters: Using slice types (&[T], &str) instead of reference-to-owned types provides better performance and more flexibility.
Performance impact:
&Vec<T> requires two pointer dereferences, &[T] requires one&mut Vec<T> to &mut [T] sped up the fastblur crate by 15%How it works:
// Slower: requires extra indirection
fn process(data: &Vec<u8>) {
for byte in data {
// Two dereferences: &Vec -> Vec -> data
}
}
// Faster: direct access
fn process(data: &[u8]) {
for byte in data {
// One dereference: &[u8] -> data
}
}
Additional benefits:
&[1, 2, 3]&vec[10..20]Bug risk: None - this is a strict improvement
Clippy lint: ptr_arg warns about this pattern
References:
Pattern: Allocating inside a hot loop when the same buffer could be reused
Why it matters: Allocation is expensive. Reusing allocations amortizes the cost across iterations.
Example:
// Slow: allocates 1000 times
for item in items {
let mut buffer = Vec::new();
process_into(&mut buffer, item);
send(buffer);
}
// Fast: allocates once, reuses buffer
let mut buffer = Vec::new();
for item in items {
buffer.clear(); // Retains capacity
process_into(&mut buffer, item);
send(&buffer);
}
Bug risk: Lifetime issues (buffer must outlive loop), state leakage between iterations (remember to clear!)
Pattern: Repeatedly allocating and deallocating expensive objects (Vec, String, complex structs)
Why it matters: Object pools amortize allocation costs across many operations and reduce allocator pressure.
Performance impact:
How it works:
// Without pooling
for _ in 0..1_000_000 {
let buffer = String::with_capacity(4096); // Allocates
use_buffer(buffer);
// Deallocates
}
// With pooling
use lifeguard::Pool;
let pool = Pool::with_size(100);
for _ in 0..1_000_000 {
let buffer = pool.detach(); // Reuses from pool
use_buffer(buffer);
// Returns to pool automatically
}
Especially valuable for:
Trade-offs:
Bug risk: State leakage between pool uses, lifecycle management complexity
References:
Pattern: Collections that grow indefinitely without limits (unbounded Vec, String, FxHashMap)
Why it matters: Unbounded growth can lead to memory exhaustion, unpredictable performance, and OOM crashes. In long-running services or load generators, this is a critical reliability concern.
How it works: Replace unbounded collections with bounded alternatives:
Example:
// Unbounded: memory grows forever
let mut events = Vec::new();
loop {
events.push(receive_event()); // Never clears, grows forever
}
// Bounded with ring buffer
use ringbuf::RingBuffer;
let mut events = RingBuffer::new(1000); // Fixed size
loop {
events.push_overwrite(receive_event()); // Overwrites oldest
}
// Bounded with capacity limit
let mut events = Vec::with_capacity(1000);
loop {
if events.len() >= 1000 {
events.clear(); // Or drain oldest half
}
events.push(receive_event());
}
Performance impact:
Trade-offs:
Bug risk: HIGH - This changes program semantics. Data loss may be unacceptable for some use cases. Requires explicit decision about:
When to use:
When NOT to use:
References:
#[inline] attribute for hot cross-crate functionsPattern: Small, frequently-called functions in library crates that aren't being inlined by caller crates
Why it matters: Rust compiles each crate separately. Without #[inline], even trivial functions cannot be inlined across crate boundaries. This prevents:
How it works:
#[inline] to include the function body in the crate metadata#[inline] (their bodies must be available for monomorphization)Example:
// In lading_payload crate - without #[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
// In lading crate - cannot inline across crate boundary
if payload.is_empty() { // Function call overhead
return;
}
// With #[inline]
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
// In lading crate - can inline
if payload.is_empty() { // Inlined, zero overhead
return;
}
When to use:
Trade-offs:
Special cases:
#[inline(always)]: Forces inlining even when compiler thinks it's not beneficial (use sparingly)Bug risk: Binary size bloat if overused
References:
.collect()Pattern: Calling .collect() between iterator operations to create intermediate collections
Why it matters: Iterator chains are zero-cost abstractions - they don't allocate until you consume them. Intermediate .collect() calls force allocation and evaluation, negating this benefit.
Example:
// Slow: allocates three times
let data = vec![1, 2, 3, 4, 5];
let filtered: Vec<_> = data.iter()
.filter(|&&x| x > 2)
.collect(); // Allocation 1
let mapped: Vec<_> = filtered.iter()
.map(|&&x| x * 2)
.collect(); // Allocation 2
let summed: Vec<_> = mapped.iter()
.take(2)
.collect(); // Allocation 3
// Fast: zero intermediate allocations
let summed: Vec<_> = data.iter()
.filter(|&&x| x > 2)
.map(|&x| x * 2)
.take(2)
.collect(); // Only one allocation
How it works:
.collect() allocatesSpecial considerations:
.chain() can be slower in some cases due to complexity.for_each() instead of .collect() if you don't need the collectionchain() + collect() may not optimize well (use for_each() instead)Bug risk: Logic errors when refactoring (order of operations matters), off-by-one errors
References:
Pattern: Cloning data when borrowing would suffice
Why it matters: Cloning allocates memory and copies data. References are zero-cost.
Example:
// Slow: unnecessary clone
fn process(data: String) {
println!("{}", data);
}
let s = String::from("hello");
process(s.clone()); // Allocates and copies
// Fast: borrow instead
fn process(data: &str) {
println!("{}", data);
}
let s = String::from("hello");
process(&s); // Zero cost
Trade-offs:
Bug risk: Lifetime complexity, borrow checker errors
Pattern: Passing or returning large structs by value
Why it matters: Passing by value requires copying the entire struct. References and Box avoid this.
Example:
struct LargeData {
buffer: [u8; 4096],
// ... more fields
}
// Slow: copies 4KB+ on every call
fn process(data: LargeData) { ... }
// Fast: zero-copy
fn process(data: &LargeData) { ... }
// Or use Box for ownership transfer
fn process(data: Box<LargeData>) { ... }
Bug risk: Nil pointer dereferences with Box, lifetime complexity with references
These optimization patterns represent well-established Rust best practices validated by:
When applied to lading's hot paths (payload generation, throttling, serialization), these techniques can yield significant performance improvements while maintaining correctness and determinism.
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer