Skip to content

Instantly share code, notes, and snippets.

@stormslowly
Created June 14, 2026 18:13
Show Gist options
  • Select an option

  • Save stormslowly/d87d39b9f67ce236fbc144309a606fa1 to your computer and use it in GitHub Desktop.

Select an option

Save stormslowly/d87d39b9f67ce236fbc144309a606fa1 to your computer and use it in GitHub Desktop.
camino Utf8Path::hash benchmark + callgrind data (delegate to inner Path, ~2.1x fewer instructions)

Utf8Path::hash micro-benchmark (callgrind)

Benchmark backing the camino PR that delegates Utf8Path::hash to the wrapped std::path::Path instead of iterating self.components().

Today impl Hash for Utf8Path walks the std::path::Components iterator and hashes each component separately. The wrapped Path::hash does the same job in a single-pass byte scan and is already consistent with eq (which compares components), so delegating to it is both correct and cheaper.

Result (Linux x86_64, valgrind 3.26 codspeed, rustc_hash::FxHasher)

strategy Ir / hash
hash_components (current: self.components()) ~1157
hash_inner (proposed: self.0.hash) ~553

~2.1x fewer instructions. std::path::Components::next alone is ~40% of the benchmark — pure traversal overhead the delegation removes. See callgrind-fxhash.txt.

How to run

Drop hash.rs into benches/ of a camino checkout and add to Cargo.toml:

[dev-dependencies]
rustc-hash = "2"

[[bench]]
name = "hash"
harness = false

Build and profile under callgrind (Linux):

cargo build --release --bench hash
BIN=$(ls -t target/release/deps/hash-* | grep -vE '\.(d|o)$' | head -1)
valgrind --tool=callgrind --instr-atstart=yes --cache-sim=no \
  --callgrind-out-file=callgrind.out "$BIN"
callgrind_annotate --inclusive=yes callgrind.out | grep hash::hash_
# callgrind (Linux x86_64, valgrind 3.26 codspeed) — Utf8Path::hash strategies
# hasher: rustc_hash::FxHasher ; each fn called 400,000x (ITERS=50_000 * 8 paths)
## inclusive Ir per strategy
691,144,119 (100.0%) PROGRAM TOTALS
462,950,000 (66.98%) ???:hash::hash_components [bench-binary]
221,250,000 (32.01%) ???:hash::hash_inner [bench-binary]
## top self (exclusive) Ir
691,144,119 (100.0%) PROGRAM TOTALS
277,250,000 (40.11%) ???:<std::path::Components as core::iter::traits::iterator::Iterator>::next [bench-binary]
221,250,000 (32.01%) ???:hash::hash_inner [bench-binary]
180,900,000 (26.17%) ???:hash::hash_components [bench-binary]
//! Callgrind benchmark for `Utf8Path` hashing strategies.
//!
//! Built as a plain `harness = false` binary so the whole process can be run
//! under `valgrind --tool=callgrind` and attributed per function via
//! `callgrind_annotate`. Each strategy lives in its own `#[inline(never)]`
//! function so its inclusive instruction count (Ir) is directly comparable.
//!
//! Hasher is `rustc_hash::FxHasher` — the cheap, word-at-a-time hasher used by
//! rustc / rspack / swc. A cheap hasher keeps byte mixing from masking the
//! path-traversal overhead, isolating the real cost difference:
//!
//! * `hash_components` — what `impl Hash for Utf8Path` does today: iterate
//! `self.components()` (a `std::path::Components` state machine) and hash each
//! component separately.
//! * `hash_inner` — the proposed fix: delegate straight to the wrapped
//! `std::path::Path` (`self.0.hash`, reached here via `as_std_path()`), which
//! hashes the raw bytes in a single pass.
use camino::Utf8Path;
use rustc_hash::FxHasher;
use std::hash::{Hash, Hasher};
use std::hint::black_box;
const CORPUS: &[&str] = &[
"Cargo.toml",
"src/lib.rs",
"crates/rspack_core/src/compiler/make/module_graph.rs",
"/home/user/project/target/release/build/foo-1234/out/bindings.rs",
"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p",
"node_modules/.pnpm/react@18.2.0/node_modules/react/index.js",
"/usr/local/lib/python3.11/site-packages/numpy/core/_multiarray_umath.so",
"deeply/nested/relative/path/to/some/source/file/that/is/long.rs",
];
#[inline(never)]
fn hash_components(p: &Utf8Path) -> u64 {
let mut h = FxHasher::default();
for component in p.components() {
component.hash(&mut h);
}
h.finish()
}
#[inline(never)]
fn hash_inner(p: &Utf8Path) -> u64 {
let mut h = FxHasher::default();
p.as_std_path().hash(&mut h);
h.finish()
}
fn main() {
const ITERS: usize = 50_000;
let utf8: Vec<&Utf8Path> = CORPUS.iter().map(|s| Utf8Path::new(s)).collect();
let mut acc = 0u64;
for _ in 0..ITERS {
for p in &utf8 {
acc ^= hash_components(black_box(p));
}
}
for _ in 0..ITERS {
for p in &utf8 {
acc ^= hash_inner(black_box(p));
}
}
black_box(acc);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment