mcpField
R
rust-performance

rust-performance

github.com/rewrite-rsOther1040 installsUpdated 10w ago
Repository

About rust-performance

A Rust coding skill for profiling and optimizing performance. It emphasizes measured changes, benchmark comparisons, allocation and collection choices, and the trade-offs of compiler settings.

Key features

  1. 01Requires profiling before optimization
  2. 02Covers Criterion and divan benchmarks
  3. 03Explains allocation reuse patterns
  4. 04Guides collection choice by access pattern
  5. 05Details hashing and codegen trade-offs

Use cases

  1. 01Finding Rust performance bottlenecks
  2. 02Comparing changes against benchmark baselines
  3. 03Reducing allocations in hot paths

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/rewrite-rs/skills --skill rust-performance

After installing, type/rust-performancein Claude Code to confirm it loaded.

Bundled filesagents/openai.yamlALLOCATION.md

Details

From the project README

Rust Performance

No change lands here that no measurement named: the profile is the entry ticket, and the codegen flags are the last five percent, not the first move. Async throughput is /async-rust; unsafe for speed is /unsafe-rust.

Measure, then change

No optimisation lands without a measurement that named it. Skipping it costs twice: the change that made nothing faster, and the change that made the code worse in exchange for nothing. A profile finds the one function that holds the time — almost never the one that looked expensive while reading.

Benchmarks that mean something

criterion for a statistical comparison against a stored baseline; divan when a lighter harness is enough. Benchmark the operation, not the setup around it.

Without it, the profiler shows addresses instead of function names:

[profile.bench]
debug = 1

A benchmark run on a laptop under thermal throttling produces a number, just not a comparable one — the machine has to be quiet between the runs being compared.

Allocation is usually the answer

The highest-yield category, and the one a profile names most often:

  • Vec::with_capacity when the size is known — the default grows and copies.
  • Clear-and-reuse over allocate-per-iteration: buffer outside, clear() inside.
  • Box<[T]> and Box<str> for an owned sequence that will never grow.
  • mem::take to move a value out of a &mut without cloning.
  • No format! in a hot loop — every call allocates a fresh String.
  • Cow for the borrow-or-own case — copy only when the value changed.

One complete example, joining with a known total size:

fn joined(parts: &[String]) -> String {
    let total = parts.iter().map(|p| p.len() + 1).sum();
    let mut out = String::with_capacity(total);
    for (i, part) in parts.iter().enumerate() {
        if i > 0 {
            out.push(' ');
        }
        out.push_str(part);
    }
    out
}

The reuse patterns and the buffer-passing shapes are in ALLOCATION.md.

Pick the collection for the access pattern

Vec beats HashMap for small n because of cache behaviour, not asymptotics. BTreeMap when iteration order matters. A sorted Vec plus binary_search when built once and read many times. The crossover is a thing to measure, not a constant to memorise.

Hashing

The default hasher is DoS-resistant and correspondingly slow. Swapping it is safe and worthwhile only when the keys are not attacker-controlled — internal IDs, enum discriminants, interned strings. With attacker-controlled keys it is a denial-of-service surface, and no lookup speed buys that back. Say the risk plainly rather than presenting the swap as a free win.

Iterators and bounds checks

An iterator chain gives the optimiser the length invariant an index loop hides, which is how bounds checks disappear: two [i] reads in one loop become iter().zip(), and the optimiser can prove the accesses in range. This is the one place where the idiomatic form is also the fast form — no tradeoff to argue about.

The last five percent

LTO, codegen-units = 1, PGO, target-cpu. Per ADR 0007: application decisions, never library defaults, each with the portability cost stated alongside the gain — a knob is one line and a profile is an afternoon:

  • target-cpu=native in committed config produces a binary that faults on an older machine.
  • PGO needs a representative workload, or it optimises for the wrong path.
  • LTO and codegen-units = 1 buy a percentage, not a factor, at the cost of build time.

panic = "abort" is not a performance default: it is an application-level choice, a library cannot assume unwinding, and it changes behaviour under #[should_panic].

Deferrals

Async throughput, executor stalls, and spawn_blocking are /async-rust — the runtime question, not the throughput one. Reaching for unsafe to skip a check is /unsafe-rust, and the answer there is that the block needs a soundness argument, not a benchmark. Whether the clone the profile found should exist at all is /ownership-not-clone.

Verification

cargo bench                  # against the recorded baseline where one exists
cargo clippy --all-targets   # at the level the repo configures; add -- -D warnings only if it configures none
cargo test --release

A benchmark with no stored baseline proves nothing about a change — record the before, then measure the after, and compare against the noise the benchmark reports. Clippy keeps the lint level the target repo configured; -D warnings is the fallback for a repo that configures no lints at all.

Comments

Markdown supported