rust-performance · diff
git:20261007.f271eea to git:20261029.a5b13dd
107 added, 374 removed. Audit A to A.
---
name: rust-performance
description: >
- Use when writing or reviewing performance-sensitive Rust, profiling slow Rust
- code, reducing allocations, investigating latency or throughput regressions,
- or diagnosing bloated binaries and long compile times. Covers allocation,
- iterators, data layout, hashing, async/concurrency, build profiles, Clippy
- review gates, reproducible baselines, profiling, and measured optimization.
- Triggers: hot path, zero-copy, with_capacity, SmallVec, Cow, size_hint,
- monomorphization, target-cpu, clippy::perf, await_holding_lock, spawn_blocking,
- Rayon, Criterion, cargo bench, samply, perf, flamegraph, heaptrack, DHAT,
- cargo-bloat, p99, performance code review.
+ Use when writing or reviewing performance-sensitive Rust, reducing allocations,
+ profiling slow code, investigating latency or throughput regressions, or diagnosing
+ binary size and compile time. Covers cost-aware implementation, representative
+ baselines, profiling, measured changes, and regression checks. Triggers include
+ hot path, zero-copy, Criterion, cargo bench, samply, perf, flamegraph, DHAT,
+ cargo-bloat, p99, contention, and performance code review.
---
# Rust Performance
- Write and review Rust with explicit ownership, allocation, layout, and concurrency
- costs. When diagnosing a slow build or claiming an improvement, define the
- user-visible metric, reproduce the workload, baseline, profile, change one cause,
- and prove correctness and improvement.
-
- Keep cold code readable. Use the writing guidance for known hot paths; use the
- measurement workflow below to establish which costs matter before optimizing.
+ Keep cold code readable. For known hot paths, make ownership, allocation,
+ layout, and concurrency costs deliberate. For an optimization claim, reproduce
+ the workload and establish before/after evidence; a plausible rewrite is not a
+ measured improvement.
- ## Quick Reference: routing
+ ## Load only the relevant reference or section
- | Task | Start here |
+ | Task | Read |
|---|---|
- | Write or review a known hot path | Mistakes and fixes; allocation through code-review gates |
- | Diagnose slowness or prove a speedup | Measurement and optimization workflow |
- | Investigate contention or multi-socket scaling | Diagnostics and optimization patterns reference |
- | Run profilers or benchmarks | Measurement and profiling reference |
-
- ## Quick Reference: mistakes and fixes
-
- | Mistake | Cost | Fix |
- |---|---|---|
- | `clone()` to get past the borrow checker, reflexively | allocation + copy per call | borrow; keep clone only where measured |
- | `Vec::new()` then `push` in a loop | realloc at caps 0,4,8,16,32,… | `with_capacity(n)` when n is known |
- | `Vec::new()` + `resize(n, 0)` | alloc + separate memset | `vec![0; n]` (uses `alloc_zeroed`) |
- | Building a `Vec` per loop iteration | one alloc per iteration | hoist the `Vec` out, `clear()` each pass |
- | `.lines()` over a file | one `String` alloc per line | `read_line` into a reused buffer |
- | `format!` on a hot path | `String` alloc | literal, or `format_args!` into a reused buffer |
- | `to_owned()`/`to_string()` in a signature | alloc per call | take `&str` + a lifetime |
- | `Box<Rc<T>>`, `Arc<&T>`, nested smart pointers | extra indirection, extra word | flatten to one owner |
- | `collect()` into a `Vec` you only iterate | alloc for nothing | return `impl Iterator` |
- | Custom iterator with no `size_hint` | repeated realloc on `collect` | implement `size_hint` |
- | `filter(..).map(..)` in a measured bottleneck | adapter overhead may survive optimization | benchmark equivalent `filter_map`, preserving predicate and output |
- | `chunks()` when the size divides evenly | remainder branch per chunk | `chunks_exact` |
- | Iterating `&T` for small `Copy` types | worse codegen than by-value | `iter().copied()` |
- | Per-element `next()` across an abstraction edge | blocks unrolling/SIMD | batch (e.g. 512) and iterate an array |
- | `LinkedList`/`Box`-chains for sequence data | pointer chase per step | `Vec`/`VecDeque`/`SmallVec` |
- | Large hot enum variant | larger values and potentially costly moves | measure layout costs and variant frequency before boxing |
- | 64-bit indices for values that fit in 8 bits | 8× the storage per index | narrow the index type |
- | Bit-packing a record for cache density | unpack work in the hot loop | measure both; packing can lose 30% |
- | Default SipHash for internal, trusted keys | hash cost on every map op | `rustc-hash`, benchmark before switching |
- | One shared atomic counter across threads | cache-line ping-pong | per-thread counters, reduce at the end |
- | Blocking or CPU-heavy work on a Tokio worker | starves other tasks | `spawn_blocking`; bound CPU concurrency or use Rayon |
- | Unbounded channel in front of a slow consumer | grows until OOM/abort | bounded `mpsc::channel(n)` |
- | `MutexGuard` held across `.await` | makes the future non-`Send`; can deadlock a worker | scope the lock, or an async mutex |
- | `spawn` per trivial item | scheduler overhead > work | batch items per task |
- | Blindly enabling `lto = "fat"` | long link, may not help | `lto = "thin"` first, measure |
- | Lock-free/`unsafe`/SIMD before the simple fix | bugs + build time for a cold win | profile first |
-
- ## Allocation
-
- Allocations are moderately expensive: a global lock, allocator bookkeeping,
- possibly a syscall. Small allocations are not automatically cheaper than large
- ones. From rustc's own experience, removing **10 allocations per million
- instructions** is worth roughly **1%** — real, but not a rewrite.
-
- 1. **Reserve when the size is known.** Growth is quasi-doubling, and the first
- capacity depends on element size — current `RawVec` starts at 8 elements for
- 1-byte types and 4 for most others, then doubles. Filling a 20-element vector
- of a moderate-size type by `push` therefore costs four allocations;
- `Vec::with_capacity(20)` costs one. The exact sequence is an implementation
- detail, not a guarantee.
- 2. **Hoist buffers out of loops.** A "workhorse" `Vec` declared outside the loop
- and `clear()`ed each iteration keeps its capacity and allocates zero times
- steady-state. Same trick for a reused `String` and `read_line`, instead of
- `BufRead::lines()` which returns `io::Result<String>` and allocates per line.
- 3. **Pass `&mut Vec`, don't return a new one**, when a function builds up a
- collection the caller already owns.
- 4. **`clone_from` reuses the destination's allocation.** `a.clone_from(&b)` beats
- `a = b.clone()` when `a` already has capacity to spare.
- 5. **`Rc`/`Arc::clone` is a refcount bump, not a copy.** Sharing one large value
- behind an `Arc` beats deep-cloning it — unless the value is rarely shared, in
- which case the refcount adds an allocation that plain ownership wouldn't need.
- 6. **`Cow` for mixed borrowed/owned data.** `Vec<Cow<'static, str>>` holds
- literals for free and only allocates for the formatted ones. Call `to_mut`
- only in the branch that needs mutable access: it clones immediately if the
- value is still borrowed, even if you never write through the reference.
- 7. **`SmallVec<[T; N]>` for many short vectors** — inline storage up to `N`, then
- spill to heap. It is *slightly slower than `Vec` for ordinary operations*
- (every op checks inline-vs-heap) and makes the struct bigger when `N` or `T`
- is large. If you know the exact maximum length, `ArrayVec` is faster because
- it never falls back. Measure before adopting either.
- 8. **Give custom iterators a `size_hint`.** `collect` uses it to reserve in one
- shot. Give the tightest truthful bound; never claim an exact count for a
- filter you haven't evaluated.
-
- ## Iterators
-
- **Myth: iterators are slower than `for` loops.** The Rust Book benchmarks both
- over the same input: `19,620,300 ns` for the explicit loop vs `19,234,900 ns`
- for the iterator chain — the same, within noise. Iterators are zero-cost; prefer
- the clearer one.
-
- **But zero-cost means "no cost versus equivalent hand-written code", not "the
- optimization opportunity survives".** Where the abstraction boundary itself is
- the problem, batch. Measured: a merge iterator driven with one `next()` call per
- item took 6.5 ms over 100k integers; batched at 512 and traversed as a plain
- array it took ~110 µs — **60×** — because the per-item call serialized the work
- and blocked unrolling and SIMD.
-
- Micro-choices that do matter inside a genuinely hot iterator:
-
- - Benchmark an equivalent `filter_map` against `filter(p).map(f)`; preserve
- the predicate and output type, including any `Option` returned by `f`.
- - Avoid `chain` in hot iterators; it is slower than a single iterator.
- - `chunks_exact` over `chunks` when the size divides the slice evenly.
- - `iter().copied()` over `iter()` for small integer types — by-value beats
- by-reference in the generated code.
- - Return `impl Iterator` from a function instead of a `Vec` when the caller
- only iterates.
-
- ## Data layout and types
-
- - **Large types can make moves expensive.** Whether a move becomes a `memcpy`
- call, inline instructions, or is eliminated depends on compiler, target, and
- context; 128 bytes is not a portable cutoff. Measure copying and layout costs.
- Boxing a large enum variant can shrink the enum, but adds allocation and
- indirection, especially costly when that variant is common. Apply
- `large_enum_variant` and `result_large_err` suggestions only after considering
- variant frequency and measuring the tradeoff.
- - **Contiguous over pointer-chasing.** `Vec`/`VecDeque`/`Box<[T]>` over
- `LinkedList`, `Vec<Box<T>>`, or linked `Box` chains.
- - **Flatten nesting and narrow indices.** In one measured case, replacing
- `Vec<Vec<usize>>` with two flat `Box<[u8]>` lists shrank a sample record from
- 216 to 48 bytes and the working set from 5.7× the text input down, for up to
- **20%** runtime.
- - **Cache misses are not the only objective.** Bit-packing one dataset's ranking
- payload into three bytes cut measured data-cache misses and made execution
- **at least 30% slower** — the unpack work in the hot loop cost more than the
- misses saved. Measure end-to-end, never proxies.
- - **Sorting helps branches and can hurt locality.** Reading pre-sorted input cut
- branch misses and gave ~14% in one benchmark. Sorting the already-allocated
- root objects instead also came out faster (~5%) than the unsorted baseline, but
- raised LLC-load misses **14×**, so it lost to reading pre-sorted input. The
- extra cache pressure ate most of the branch-prediction win. If you sort for
- locality, say so in a comment — it's fragile.
-
- ## Hashing
-
- The default `HashMap` hasher is SipHash 1-3: collision-resistant and
- deliberately slow, especially for short and integer keys. Measured inside rustc:
- `fnv → fxhash` gave **up to 6%** faster, but `fxhash → ahash` was **1–4%
- slower** and `fxhash → default` was **4–84% slower**. Faster is not better —
- measure your key distribution.
-
- Switch only when profiling says hashing is hot, and never for keys an untrusted
- party controls (the DoS resistance is the point of the default).
-
- ## Concurrency and async
-
- - **A mutex can beat a naive atomic under contention.** One shared counter:
- 2 threads → atomics ~5× faster; 8 threads → mutex ~1.34× faster, because
- cache-line ownership handoff dominates when every thread hammers the same
- line. The mutex arbitrates and reduces simultaneous hammering. Fix the
- sharing rather than the primitive: per-thread counters merged at the end, then
- the atomic is uncontended again.
- - **There is no universally faster mutex.** Short critical sections and moderate
- contention favored `std` (**9%** higher throughput in one benchmark).
- Bursty loads favored `parking_lot` (**18.5%** higher throughput). Where a
- thread holds the lock for 500 µs, `std` starved other threads (95% variation)
- and `parking_lot` did not (1.9%) at the cost of ~7.5% throughput — its
- eventual-fairness handoff fires around 0.5 ms. Sleeping or doing I/O under a
- lock is the actual bug in that scenario.
- - **Tokio is not preemptive.** Bound uninterrupted work between yields, not
- total task lifetime: long-lived async tasks are fine when they yield regularly.
- Move blocking operations to `spawn_blocking`. For many CPU-heavy jobs, limit
- concurrency with a semaphore or use a bounded CPU executor such as Rayon;
- the blocking pool's large default thread limit is not a CPU concurrency budget.
- - **Bounded channels.** An unbounded channel has no backpressure and buffers
- until memory is exhausted — the only bound is the machine, and exhaustion can
- abort the process. Use `mpsc::channel(n)` and pick `n` from the workload.
- - **Never hold a `MutexGuard` across `.await`** (nor a `RefCell` borrow). Scope
- the lock, or use an async-aware mutex. This is a correctness bug first.
- - **Don't `block_on` inside a runtime worker**, and don't spawn a task per
- trivial item — batch. Worker count defaults to one per CPU core; only override
- it after measuring, and remember container CPU quotas.
- - **Rayon has no magic threshold.** Trivial work can lose outright: a forum
- report of a 50k `i32` sum showed the sequential version ~9× faster, and a
- separate participant's playground check showed the sequential sum constant-fold
- to a single `mov` while Rayon kept its scheduling machinery. Tuning splitting is
- not one-directional either: `with_max_len(1)` measured **2× slower than a
- specialized custom pool** on random input while helping skewed input. Tune
- `with_min_len`/`with_max_len` against a real workload.
-
- ## Build and compile configuration
-
- Only the workspace-root `Cargo.toml` profile settings apply.
+ | Allocation, buffer reuse, cloning, small collections | [Allocation](references/anti-patterns.md#1-allocation) |
+ | Iterator overhead, batching, layout, or hashing | [Iterators](references/anti-patterns.md#2-iterators), [layout](references/anti-patterns.md#3-data-layout), or [hashing](references/anti-patterns.md#4-hashing) |
+ | Async worker stalls, locks, queues, task granularity | [Concurrency](references/anti-patterns.md#5-concurrency) |
+ | Compiler settings, binary/build cost, lint review | [Build configuration](references/anti-patterns.md#6-build-configuration) or [Clippy](references/anti-patterns.md#7-clippy-gates) |
+ | Layout changes, sharding, SIMD, or NUMA | [Optimization patterns](references/optimization-patterns.md) |
+ | Profiler/benchmark commands and result reporting | [Measurement and profiling](references/measurement-and-profiling.md) |
- | Setting | Effect |
- |---|---|
- | `--release` vs dev | 10–100×. Never tune a debug build |
- | `codegen-units = 1` | better runtime and smaller binary, slower compile (default: 256 incremental / 16 otherwise) |
- | `lto = "thin"` | 10–20% possible, substantially faster to link than fat |
- | `lto = "fat"` | more aggressive, **may not improve** speed or size |
- | `opt-level = 3` | can be *slower* than `2`; `"z"` disables loop vectorization |
- | `target-cpu = native` | emits AVX etc.; loses portability of the binary |
- | PGO | 10%+ possible; needs a representative profile run; unsupported for crates.io binaries |
- | `panic = "abort"` | smaller binary, slightly faster, less compile time — no unwinding |
- | Allocator swap | measure; on `musl` targets the default allocator is a known cliff |
+ Historical benchmark figures and their conditions live in the references. They
+ suggest experiments; they do not predict a speedup in the current project.
- **Monomorphization is the compile-time tax on generics.** Every distinct type
- combination is a fresh instantiation. In one project, replacing real
- element-type generics with a single shared type cut incremental compile time from
- 108s to 14s; across that project's whole optimization pass the binary went 29 MB
- → 2.5 MB. Find the bloat with `cargo llvm-lines` (it reports generated LLVM IR
- lines and the number of copies of each function); confirm with
- `-Zprint-mono-items`.
+ ## Establish the task and contract
- **Compile-time levers, in order:** `cargo build --timings` to find the blocking
- crate → `cargo check` in the inner loop (**2–3×** faster than `cargo build` when
- no artifact is needed) → `[profile.dev] debug = "line-tables-only"` and
- `debug = false` for dependencies (2–30% cycle improvement in compiler
- benchmarks) → `lld`/`mold` if linking dominates → trim dependencies and features.
- Trimming features does *not* always help; one large project got slower. Proc
- macros are the worst offenders — complex ones take 20+ seconds to compile.
+ 1. Read the callers, repository instructions, toolchain, edition/MSRV, targets,
+ feature selection, allocator, and build profiles. Preserve compatibility and
+ the actual deployment constraints.
+ 2. Separate **initial writing/review** from **diagnosing a regression or claiming
+ a speedup**. Ordinary ownership choices do not require a profiler. New
+ performance-motivated complexity requires evidence that its cost matters.
+ 3. Name the user-visible metric: latency distribution, throughput, peak/retained
+ memory, allocations, artifact size, or developer build time. State acceptable
+ tradeoffs and the workload that must retain its behavior.
- ## Code-review gates
+ ## Writing and review gates
- `clippy::perf` is warn-by-default, so plain `cargo clippy` already catches the
- mechanical half of the table above. The `suspicious` lints below are also
- warn-by-default. Opt in to the rest explicitly:
+ - **Preserve semantics before reducing work.** Keep parsing errors, text
+ terminators, ordering, floating-point requirements, and partial I/O behavior.
+ Reusing a line buffer must not turn precise newline removal into `trim()`.
+ - **Give ownership a reason.** Borrow for inspection; retain ownership when the
+ caller needs an independent value. `Arc::clone` shares a handle; an owned
+ parameter can accept a move without allocating. Do not remove useful clones
+ merely to satisfy a blanket rule.
+ - **Make repeated storage costs visible.** Reserve a known bound, reuse buffers
+ where lifetimes permit, and avoid collecting solely to iterate. Account for
+ retained capacity; `SmallVec`, pooling, and interning introduce their own costs.
+ Iterator bounds must be truthful, not optimistic allocation hints.
+ - **Treat micro-rewrites as candidates.** Iterator versus loop, `filter_map`,
+ `chain`, copied items, boxing, and inlining have workload-dependent outcomes.
+ Preserve the contract and measure the actual hot path before preferring one.
+ - **Check layout and key domains.** Boxing trades inline size for allocation and
+ indirection. Narrow indices only after validating bounds. Preserve established
+ `repr(C)` layouts. Change a hasher only after measuring and reviewing whether
+ keys can be attacker-controlled.
+ - **Keep async workers available.** Move blocking work off runtime workers;
+ bound CPU concurrency and queued/task state. A blocking mutex guard should not
+ span `.await`; an async mutex may, but still needs a cancellation and wait-cycle
+ review. A channel capacity alone does not bound all waiting tasks or payloads.
+ - **Diagnose sharing before padding.** Contention on the same atomic needs less
+ sharing, batching, or sharding. Distinct hot locations on one cache line may
+ need padding; confirm the addresses and target layout first.
+ - **Review configuration as a tradeoff.** LTO, codegen units, target CPU, allocator,
+ and PGO can change portability, size, and build time as well as execution time.
+ Clippy suggestions are review prompts; use the project's selected lint policy.
- | Lint | Group | Catches |
- |---|---|---|
- | `slow_vector_initialization` | perf (warn) | `Vec::new()` + `resize` instead of `vec![0; n]` |
- | `vec_init_then_push` | perf (warn) | `Vec::new()` followed by literal pushes |
- | `useless_vec` | perf (warn) | `&vec![..]` where `&[..]` works |
- | `large_enum_variant` | perf (warn) | one oversized enum variant |
- | `result_large_err` | perf (warn) | oversized `Result::Err`, which propagates upward |
- | `boxed_local` | perf (warn) | `Box<T>` where `T` on the stack is fine |
- | `manual_memcpy` | perf (warn) | element-by-element copy loop |
- | `redundant_allocation` | perf (warn) | `Box<Rc<T>>`, `Arc<&T>`, nested owners |
- | `unnecessary_to_owned` | perf (warn) | `to_owned()`/`to_string()` that allocates needlessly |
- | `manual_str_repeat` | perf (warn) | hand-rolled `repeat` |
- | `needless_pass_by_value` | pedantic | by-value params that aren't consumed (may force a clone) |
- | `inefficient_to_string` | pedantic | `.to_string()` on `&&T` — bypasses the specialized impl |
- | `single_char_pattern` | pedantic | `"x"` where `'x'` works (perf inconclusive; clarity win) |
- | `redundant_clone` | nursery (allow) | `clone()` of a value about to be dropped — LLVM often can't remove it |
- | `or_fun_call` | nursery (allow) | `unwrap_or(expensive())` instead of `unwrap_or_else` |
- | `needless_collect` | nursery (allow) | `collect().len()` |
- | `large_stack_frames` | nursery (allow) | huge stack allocations |
- | `await_holding_lock` | suspicious | `MutexGuard` across `.await` |
- | `await_holding_refcell_ref` | suspicious | `RefCell` borrow across `.await` |
+ ## Diagnose and measure
- Clippy's own groups: `perf` is "compiler can't trivially optimize this, but a
- slightly different expression helps" — easy to apply. `pedantic` false positives
- are intentional; cherry-pick lints rather than enabling the whole group.
+ ### 1. Define a reproducible workload
- ## Measurement and optimization workflow
+ Record dataset shape, inputs, concurrency, CPU/memory limits, hardware, dependency
+ and compiler versions, target/features, allocator, profile, warm-up, and cache
+ state. Separate steady state from startup/shutdown and throughput from tail
+ latency. Use the deployed profile unless debug/build performance is the actual
+ problem; say explicitly when a synthetic workload is the intended target.
- Optimization priority — always fix the highest rung first:
+ ### 2. Preserve a baseline and correctness oracle
- ```text
- 1. Algorithm choice 10x-1000x biggest impact
- 2. Data structure 2x-10x
- 3. Reduce allocations 2x-5x
- 4. Cache locality 1.5x-3x
- 5. SIMD / parallelism 2x-8x
- ```
+ Run the relevant existing checks. Before changing parsing, I/O, numeric behavior,
+ or another fragile contract, add a focused regression if coverage is missing.
+ Record the baseline artifact/revision and raw samples with dispersion. Control
+ background load, thermal effects, and noisy runners enough to resolve the desired
+ change. Decide whether setup, input restoration, and destruction belong in timing.
- Pick the measurement layer before touching code:
+ ### 3. Choose the measurement layer
- | Question | Tool |
+ | Question | Candidate evidence |
|---|---|
- | Is a pure operation faster? | Criterion / Divan microbenchmark |
- | Did instruction-level cost change? | iai-callgrind (deterministic, CI-friendly) |
- | Where is CPU time spent? | samply, `cargo flamegraph`, perf, Instruments |
- | Where are allocations retained? | DHAT, heaptrack, allocator metrics |
- | Why is the binary large? | `cargo bloat`, feature inspection |
- | What drives compile time? | `cargo build --timings`, `cargo llvm-lines` |
- | Does the service meet its SLO? | representative load test + observability |
-
- A microbenchmark never proves end-to-end latency, overload behavior, or memory
- bounds. Match the tool to the question.
-
- ### Evidence pipeline
-
- #### 1. Define the performance contract
-
- Record workload, dataset, inputs, concurrency, platform, CPU/memory limits,
- toolchain, target, features, allocator, build profile, warm-up, cache state,
- and the success metric. Separate throughput from p50/p95/p99 latency, and
- steady state from startup/shutdown.
-
- Do not optimize debug builds or synthetic inputs unless they are the real
- problem.
-
- #### 2. Establish a reproducible baseline
-
- - Verify correctness before benchmarking.
- - Use release-like settings (opt-level, codegen-units, LTO, panic strategy,
- target-cpu) deliberately and record them.
- - Isolate background load, thermal throttling, and noisy shared runners.
- - Keep raw samples and variance, not one average.
-
- #### 3. Add regression tests before optimizing
-
- **Correctness-first gate**: if the change touches parsing, I/O, or float
- formatting, add or extend the regression test *before* benchmarking.
-
- ```text
- 1. BASELINE cargo test current behavior pinned
- 2. TEST add regression tests for the code you will change
- 3. OPTIMIZE change one cause
- 4. VERIFY cargo test correctness preserved
- 5. BENCHMARK cargo bench improvement measured
- ```
-
- #### 4. Profile before editing
-
- Capture a profile under the failing workload with symbols preserved, using the
- same optimized artifact you intend to compare. Separate on-CPU work, waiting,
- lock contention, I/O, allocation, page faults, and scheduler overhead.
-
- #### 5. Optimize the dominant cause
-
- Common wins: algorithmic complexity, fewer passes, batching, avoiding repeated
- parsing or allocation, borrowing instead of cloning, data-layout changes,
- reduced synchronization, bounded queues, streaming, feature reduction, moving
- CPU work off async workers.
-
- Unsafe, custom allocators, SIMD, lock-free structures, and caching are last
- resorts — only after simpler changes fall short, and only with documented
- invariants plus tests.
-
- #### 6. Track memory, size, and build cost
-
- Peak RSS, retained heap, allocation rate, fragmentation, cache growth, buffer
- bounds, binary sections, monomorphization, debug info, enabled features, macro
- expansion, incremental versus clean compile time.
-
- #### 7. Prove the result
-
- Rerun correctness tests and the exact baseline protocol. Report absolute and
- relative numbers, variance, hardware/software context, tradeoffs, and any
- secondary-metric regression. Add a durable benchmark or budget only when the
- environment reliably detects the threshold.
-
- ## Hard rules
-
- - Do not add performance-motivated complexity to cold code. Ordinary cloning,
- boxing, and dependencies required for functionality or readable ownership
- remain valid; do not replace them with speculative optimizations.
- - Do not claim a speedup without a before/after number on the same workload.
- - Do not trade correctness or safety for speed. `unsafe` needs documented
- invariants plus tests.
- - Do not add a faster hasher, allocator, or build flag "just in case". Each has
- a documented case where it loses.
- - Do not report cache misses, instruction counts, or branch misses as the
- result. Report end-to-end time.
-
- - Document the build profile for every benchmark. Profile the dominant cost
- before performance-motivated changes; initial writing does not require a profile.
+ | Does one operation cost less? | Criterion/Divan with a representative input and timed region |
+ | Did instruction-level work change? | iai-callgrind with controlled inputs and configuration |
+ | Where is CPU time spent? | samply, flamegraph, perf, or platform profiler with symbols |
+ | Where does memory go? | Allocation/retention profiler, allocator metrics, and peak RSS |
+ | Why are build/artifact costs high? | `cargo build --timings`, `cargo llvm-lines`, `cargo bloat`, feature inspection |
+ | Does a service meet its objective? | Representative load, latency distribution, overload behavior, observability |
- ## Diagnostics: symptom to cause
+ A microbenchmark does not establish service latency or memory bounds. Hardware
+ counters explain a result; they do not replace the user-visible metric.
- | Symptom | Likely cause | Fix |
- |---|---|---|
- | One core at 100%, many atomic RMW ops, more threads = slower | False sharing | `#[repr(align(64))]` per counter |
- | Time in lock/unlock, degrades with threads | Lock contention | thread-local sharding, merge at end; DashMap for concurrent writes |
- | Scaling stalls on multi-socket | Cross-NUMA access | `numactl --cpunodebind/--membind`, NUMA-aware pools |
- | Allocator pressure, many small allocs | Per-item allocation | preallocate, reuse buffers, `SmallVec`, object pooling |
- | O(n²) growth in a loop | Repeated concatenation / rehash | `with_capacity`, batch ops, integer keys over strings |
- | High LLC misses on linear scans | Pointer chasing / poor layout | SoA instead of AoS, `Vec`/`VecDeque` over `LinkedList` / `Box` chains |
+ ### 4. Find and change the dominant cause
- Patterns, code, and expanded triage tables:
- [references/optimization-patterns.md](references/optimization-patterns.md).
- Tooling, benchmark templates, build profiles, and report format:
- [references/measurement-and-profiling.md](references/measurement-and-profiling.md).
+ Profile the representative workload with symbols using the artifact being
+ compared. Distinguish CPU work from waiting, locks, I/O, allocation, page faults,
+ and scheduler overhead. Prioritize the measured bottleneck; simpler algorithm,
+ representation, and allocation changes are good candidates when they address it.
+ There is no fixed optimization order or portable speedup multiplier.
- ## Completion Criteria
+ Change one cause at a time. Unsafe code, custom allocation, SIMD, lock-free
+ structures, and caches require a concrete need, explicit invariants, and tests;
+ prefer a simpler adequate change. Preserve synchronization and lifetime contracts.
- - Representative workload and user-visible metric defined.
- - Reproducible baseline recorded before the change.
- - Dominant cost identified by profile, not guessed.
- - Correctness preserved, proven by targeted tests.
- - Re-measured under the same protocol; uncertainty and tradeoffs reported.
- - Stable regression check added, or the environment's noise floor explained.
+ ### 5. Re-measure and report
- ## Data Privacy
+ Run the correctness checks and the same baseline protocol on the candidate.
+ Report absolute and relative numbers, variation, revisions, environment, and
+ tradeoffs. Check relevant secondary metrics: tail latency, peak/retained memory,
+ allocation rate, binary size, compile time, or startup cost. Retain a durable
+ benchmark/budget only where its noise floor can detect the intended regression.
- This skill does not collect, transmit, or store data. Profiles, heap dumps,
- symbols, and benchmark datasets can contain sensitive information — confirm
- storage and upload policy before sharing them.
+ ## Completion criteria
- ## Depth and sources
+ - Writing/review decisions preserve behavior and avoid speculative complexity.
+ - Optimization claims identify the workload, baseline, dominant cost, and changed
+ cause; no claim exceeds the measurements actually performed.
+ - Correctness checks cover the changed contract and report their execution scope.
+ - Before/after results include uncertainty and relevant secondary regressions.
+ - Missing tools, untested targets, and proposed checks remain explicit limits.
- - [Anti-patterns](references/anti-patterns.md): full mistake catalog, costs, evidence, and fixes.
- - [Sources](references/sources.md): writing guidance sources, primary and secondary, with claim attribution.
- - [Optimization patterns](references/optimization-patterns.md): layout, SIMD, false sharing, contention, and NUMA examples.
- - [Measurement and profiling](references/measurement-and-profiling.md): tooling, benchmark templates, profiles, and reporting.
+ Profiles, symbols, heap dumps, and datasets may contain sensitive information;
+ follow the project's storage/sharing policy. [Sources](references/sources.md)
+ provide attribution and deeper evidence, not a required preload.