agentleFS
adxptivedadxptived/Rust-Skills/skills/rust-performance/SKILL.md

rust-performance

Skill1 starsChanged 6 months agoMITView on GitHub

Rust performance optimization, profiling, and benchmarking skill. ALWAYS use this skill when: - User says Rust code is "slow", "takes too long", or needs to be "faster" - User wants to profile CPU usage, memory allocations, or find bottlenecks - User asks about flamegraph, perf, criterion, DHAT, heaptrack, or any profiling tool - User is concerned about allocations, cloning, or memory efficiency - User asks about Vec::with_capacity, Cow, SmallVec, or allocation strategies - User wants to write benchmarks or compare performance of implementations - User asks about release mode, LTO, codegen-units, or compiler optimizations - User mentions "too many allocations", "memory usage", or "throughput" - User works with large datasets, parsing, or performance-critical code Even for general "make this faster" requests on Rust code, use this skill.

  • Installs packages

7 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use rust-unsafe-ffi and rust-macros.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • botirkhaltaevSkill

    rust-unsafe-ffi

    Master unsafe Rust, FFI, and foreign function interface programming. Use when writing unsafe Rust code with raw pointers, interfacing with C/C++ libraries, creating bindings with bindgen, implementing zero-cost abstractions, working with uninitialized memory, or understanding the safety/unsafety boundary.

    Also in 3 projects that use this

  • dawidpereiraSkill

    rust-macros

    Rust macros — macro_rules!, proc macros, derive macros, attribute macros, fragment specifiers, repetition, hygiene, cargo expand, when to use macros vs generics. Use when writing or debugging declarative or procedural macros, choosing between macros and other abstractions, understanding expansion errors, or generating repetitive code.

    Also in 4 projects that use this

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 6 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Benchmark changes: - Verify improvements

    line 31
  2. Distinctive:

    Always benchmark in release mode with optimizations:

    line 36
  3. Very specific:

    Use black_box() to prevent optimization

    line 193

“Common” is measured: kept by at least 6 in 10 of the 6 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust Performance Optimization

Comprehensive guide to profiling, measuring, and optimizing Rust code. Based on "The Rust Performance Book" and real-world optimization patterns.

Quick Navigation

  • references/profiling.md - Profiling tools and techniques
  • references/allocations.md - Reducing heap allocations
  • references/concurrency.md - Parallel and async optimization
  • references/compiler_optimizations.md - Release profiles, LTO, PGO, CPU targets

Golden Rules

  1. Measure first - Profile before optimizing
  2. Optimize hot paths - 90% of time in 10% of code
  3. Benchmark changes - Verify improvements
  4. Consider tradeoffs - Speed vs memory vs complexity

Release Mode

Always benchmark in release mode with optimizations:

# Cargo.toml
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"

# For profiling (symbols but optimized)
[profile.release-with-debug]
inherits = "release"
debug = true
cargo build --release
cargo run --release

Quick Performance Wins

1. Use &str Instead of String

// Allocates on every call
fn greet_slow(name: String) {
    println!("Hello, {}!", name);
}

// Zero allocation
fn greet_fast(name: &str) {
    println!("Hello, {}!", name);
}

2. Pre-allocate Collections

// Reallocates as it grows
let mut v = Vec::new();
for i in 0..1000 {
    v.push(i);
}

// Single allocation
let mut v = Vec::with_capacity(1000);
for i in 0..1000 {
    v.push(i);
}

// Even better: use iterators
let v: Vec<_> = (0..1000).collect();

3. Avoid Unnecessary Clones

// Bad: clones unnecessarily
fn process(items: Vec<Item>) -> Vec<Result> {
    items.iter()
        .map(|item| item.clone())  // Don't clone if you don't need it
        .map(|item| transform(item))
        .collect()
}

// Good: take ownership or borrow
fn process(items: Vec<Item>) -> Vec<Result> {
    items.into_iter()  // Consume the Vec
        .map(transform)
        .collect()
}

4. Use Cow<str> for Maybe-Owned Strings

use std::borrow::Cow;

fn normalize(input: &str) -> Cow<'_, str> {
    if input.contains(' ') {
        Cow::Owned(input.replace(' ', "_"))
    } else {
        Cow::Borrowed(input)  // No allocation
    }
}

5. Use collect() Strategically

// Creates intermediate Vec
let sum: i32 = items.iter()
    .map(|x| x * 2)
    .collect::<Vec<_>>()  // Unnecessary
    .iter()
    .sum();

// Direct iteration
let sum: i32 = items.iter()
    .map(|x| x * 2)
    .sum();

Benchmarking

Criterion.rs

The gold standard for Rust benchmarks:

[dev-dependencies]
criterion = "0.5"

[[bench]]
name = "my_benchmark"
harness = false
// benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn fibonacci(n: u64) -> u64 {
    match n {
        0 | 1 => n,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn criterion_benchmark(c: &mut Criterion) {
    c.bench_function("fib 20", |b| {
        b.iter(|| fibonacci(black_box(20)))
    });
    
    // Compare implementations
    let mut group = c.benchmark_group("String Ops");
    group.bench_function("clone", |b| {
        b.iter(|| String::from("hello").clone())
    });
    group.bench_function("to_string", |b| {
        b.iter(|| "hello".to_string())
    });
    group.finish();
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
cargo bench
cargo bench -- "fib"  # Run specific benchmarks

Key Points

  • Use black_box() to prevent optimization
  • Multiple iterations for statistical significance
  • Compare baseline vs optimized
  • Watch for outliers

Profiling

CPU Profiling

perf (Linux):

perf record -g --call-graph dwarf target/release/myapp
perf report

flamegraph:

cargo install flamegraph
cargo flamegraph --bin myapp
# Open flamegraph.svg in browser

samply (Cross-platform):

cargo install samply
samply record target/release/myapp

Memory Profiling

DHAT (Heap profiling):

[dependencies]
dhat = "0.3"
#[cfg(feature = "dhat")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;

fn main() {
    #[cfg(feature = "dhat")]
    let _profiler = dhat::Profiler::new_heap();
    
    // Your code here
}

Heaptrack (Linux):

heaptrack target/release/myapp
heaptrack --analyze heaptrack.myapp.*.zst

Finding Allocations

// Track allocations with a custom allocator
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

static ALLOCATED: AtomicUsize = AtomicUsize::new(0);

struct CountingAlloc;

unsafe impl GlobalAlloc for CountingAlloc {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        ALLOCATED.fetch_add(layout.size(), Ordering::SeqCst);
        System.alloc(layout)
    }
    
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        ALLOCATED.fetch_sub(layout.size(), Ordering::SeqCst);
        System.dealloc(ptr, layout)
    }
}

#[global_allocator]
static A: CountingAlloc = CountingAlloc;

fn main() {
    let before = ALLOCATED.load(Ordering::SeqCst);
    // Code to measure
    let after = ALLOCATED.load(Ordering::SeqCst);
    println!("Allocated {} bytes", after - before);
}

Common Optimizations

String Operations

// Slow: Many allocations
let mut s = String::new();
for i in 0..100 {
    s = s + &i.to_string();  // New allocation each time
}

// Better: Single buffer
let mut s = String::with_capacity(300);
for i in 0..100 {
    use std::fmt::Write;
    write!(s, "{}", i).unwrap();
}

// Best: Join iterator
let s: String = (0..100).map(|i| i.to_string()).collect();

Hash Maps

use std::collections::HashMap;

// Pre-size when you know the count
let mut map = HashMap::with_capacity(1000);

// Use entry API (single lookup)
*map.entry(key).or_insert(0) += 1;

// Consider FxHashMap for integer keys
use rustc_hash::FxHashMap;
let mut map: FxHashMap<u64, Value> = FxHashMap::default();

Iterators vs Loops

// Iterators often optimize better
let sum: i32 = data.iter().map(|x| x * 2).filter(|x| *x > 10).sum();

// But sometimes explicit loops are clearer and equally fast
let mut sum = 0;
for x in &data {
    let doubled = x * 2;
    if doubled > 10 {
        sum += doubled;
    }
}

Avoid Bounds Checking

// Bounds check on each access
for i in 0..v.len() {
    process(v[i]);
}

// No bounds checks (iterator)
for item in &v {
    process(*item);
}

// Unsafe: skip bounds check (only when proven safe!)
unsafe {
    for i in 0..v.len() {
        process(*v.get_unchecked(i));
    }
}

Stack vs Heap

// Heap allocated (Box)
let data = Box::new([0u8; 1024]);

// Stack allocated (if small enough)
let data = [0u8; 1024];

// Use arrays for fixed-size, known at compile time
// Use Vec for dynamic size

Async Performance

// Avoid holding locks across awaits
let data = {
    let guard = mutex.lock().await;
    guard.clone()  // Clone and release lock
}; // Lock released here
process(data).await;  // Await without lock

// Use tokio::spawn for CPU-bound work
let result = tokio::task::spawn_blocking(|| {
    expensive_computation()
}).await?;

// Buffer I/O
use tokio::io::BufReader;
let reader = BufReader::new(file);

Data Structure Choice

Use CaseData StructureWhy
Sequential accessVec<T>Cache-friendly
Key-value lookupHashMap / FxHashMapO(1) average
Sorted + lookupBTreeMapO(log n), ordered
Unique elementsHashSetO(1) contains
FIFO queueVecDequeO(1) push/pop both ends
Small fixed setArrayVec / SmallVecNo heap allocation
Bit flagsbitflagsMemory efficient

Compiler Hints

// Likely/unlikely branches (nightly)
#![feature(core_intrinsics)]
use std::intrinsics::{likely, unlikely};

if unlikely(error_condition) {
    handle_error();
}

// Inline hints
#[inline]           // Suggest inlining
#[inline(always)]   // Force inlining
#[inline(never)]    // Prevent inlining
#[cold]             // Rarely called (optimize for space)
fn rarely_used() { ... }

// Target-specific optimization
#[cfg(target_feature = "avx2")]
fn simd_process(data: &[f32]) { ... }

Arena Allocators

For batch allocations freed together (parsers, request processing):

use bumpalo::Bump;

// All allocations from one arena — freed together when arena drops
fn parse<'a>(input: &str, arena: &'a Bump) -> Vec<&'a Node> {
    tokenize(input).map(|t| arena.alloc(Node::new(t))).collect()
}

// Per-request: allocate freely, free everything at once
async fn handle(req: Request) -> Response {
    let arena = Bump::new();
    let parsed = parse(&req.body, &arena);
    generate_response(parsed)
} // arena dropped: all parsed nodes freed in one dealloc

SmallVec — Inline Small Collections

use smallvec::SmallVec;

// Up to 4 items stored inline (stack), spills to heap beyond that
let mut tags: SmallVec<[&str; 4]> = SmallVec::new();
tags.push("performance");
tags.push("rust");
// No heap allocation for ≤ 4 items

Write! Over format!

use std::fmt::Write;

// Bad: allocates a new String every time
let s = format!("key={} val={}", key, val);

// Good: write into pre-allocated buffer, clear and reuse
let mut buf = String::with_capacity(128);
for (key, val) in &map {
    buf.clear();
    write!(buf, "key={key} val={val}").unwrap();
    send(&buf);
}

Entry API for HashMap

// Bad: two lookups
if let Some(v) = map.get_mut(&k) { *v += 1; } else { map.insert(k, 1); }

// Good: single lookup
*map.entry(k).or_insert(0) += 1;

Struct Size — Keep It Small

use std::mem::size_of;

// Catch size regressions at compile time
const _: () = assert!(size_of::<MyEvent>() <= 64);

// Large enum variant → box it
enum Message {
    Ping,
    Text(String),
    // HugeThing(VeryLargeStruct), // Makes ALL variants huge!
    HugeThing(Box<VeryLargeStruct>), // Pointer-sized, heap only when needed
}

Advanced Compiler Optimizations

1. Compile with CPU Native Target

In release mode, use target-cpu=native to allow the compiler to generate machine instructions specifically optimized for the host CPU (enabling AVX, SSE4, etc.).

RUSTFLAGS="-C target-cpu=native" cargo build --release

2. Instruct the Compiler on Cold Paths

Mark rarely executed logic (like initialization, configuration loading, or panic routes) with the #[cold] attribute. This instructs the compiler to optimize the layout of code instructions to prioritize hot execution flows.

#[cold]
fn parse_dev_configurations() {
    // rarely called; keeps instruction caches clean
}

3. Explicit SIMD Vectorization

For high-performance numerical routines, consider using std::simd to process multiple elements in parallel.

#![feature(portable_simd)]
use std::simd::f32x4;

pub fn add_vectors(a: &[f32; 4], b: &[f32; 4]) -> [f32; 4] {
    let sa = f32x4::from_slice(a);
    let sb = f32x4::from_slice(b);
    let sum = sa + sb;
    let mut out = [0.0; 4];
    sum.copy_to_slice(&mut out);
    out
}

Quick Checklist

  • Profiled to find actual bottleneck?
  • Running in release mode?
  • Using &str instead of String where possible?
  • Pre-allocating collections?
  • Avoiding unnecessary clones?
  • Using iterators over index loops?
  • Using appropriate data structures?
  • Benchmarked before and after?
  • No intermediate .collect() in hot paths?
  • Using entry API for HashMap inserts?
  • Considered SmallVec or arena for short-lived allocations?

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.