From 36a1e850559fdaf3ef2656dda7fdc17ef7b6fa13 Mon Sep 17 00:00:00 2001 From: MesTTo Date: Sun, 5 Jul 2026 03:59:39 +1000 Subject: [PATCH] Add a ZipperHeadOwned vs ZipperHead benchmark Answers the existing TODO on ZipperHeadOwned: compares same-thread read creation and write creation/cleanup against the ordinary ZipperHead, at 1/10/100 repeats. Owned pays a small, roughly constant tax for the Mutex it needs to be Sync (read creation ~150ns vs ~140-150ns borrowed; write creation/cleanup ~280-300ns vs ~260-270ns borrowed), consistent with locking overhead rather than scaling with activity. --- Cargo.toml | 4 ++ benches/zipper_head_owned.rs | 97 ++++++++++++++++++++++++++++++++++++ src/zipper_head.rs | 8 ++- 3 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 benches/zipper_head_owned.rs diff --git a/Cargo.toml b/Cargo.toml index d962249..29a2eb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,6 +128,10 @@ harness = false name = "graft_masked_branches" harness = false +[[bench]] +name = "zipper_head_owned" +harness = false + [workspace] members = ["pathmap-derive", "examples/sampling", "examples/arena_compact_tests"] resolver = "2" diff --git a/benches/zipper_head_owned.rs b/benches/zipper_head_owned.rs new file mode 100644 index 0000000..4b4d032 --- /dev/null +++ b/benches/zipper_head_owned.rs @@ -0,0 +1,97 @@ +use divan::{Bencher, Divan, black_box}; +use pathmap::PathMap; +use pathmap::zipper::*; + +fn main() { + Divan::from_args().sample_count(100).main(); +} + +fn zipper_head_fixture() -> PathMap { + let mut map = PathMap::new(); + for group in 0..16u8 { + for leaf in 0..16u8 { + map.set_val_at([group, leaf], ((group as usize) << 8) | leaf as usize); + } + } + map +} + +fn bench_read_creation(bencher: Bencher, repeats: usize, mut read_child_count: F) +where + F: FnMut() -> usize, +{ + bencher.bench_local(|| { + let mut observed = 0usize; + for _ in 0..repeats { + observed += read_child_count(); + } + black_box(observed); + }); +} + +fn bench_write_creation_cleanup(bencher: Bencher, repeats: usize, mut create_and_cleanup: F) +where + F: FnMut([u8; 2]) -> usize, +{ + bencher.bench_local(|| { + let mut observed = 0usize; + for i in 0..repeats { + observed += create_and_cleanup([240u8, i as u8]); + } + black_box(observed); + }); +} + +fn bench_head_read_creation<'trie, H>(bencher: Bencher, repeats: usize, head: &H) +where + H: ZipperCreation<'trie, usize>, +{ + let path = [7u8]; + + bench_read_creation(bencher, repeats, || { + let reader = head.read_zipper_at_borrowed_path(black_box(&path)).unwrap(); + reader.child_count() + }); +} + +fn bench_head_write_creation_cleanup<'trie, H>(bencher: Bencher, repeats: usize, head: &H) +where + H: ZipperCreation<'trie, usize>, +{ + bench_write_creation_cleanup(bencher, repeats, |path| { + let writer = head + .write_zipper_at_exclusive_path(black_box(path)) + .unwrap(); + let observed = writer.path_exists() as usize; + head.cleanup_write_zipper(writer); + observed + }); +} + +#[divan::bench(args = [1usize, 10, 100])] +fn borrowed_head_read_creation(bencher: Bencher, repeats: usize) { + let mut map = zipper_head_fixture(); + let head = black_box(&mut map).zipper_head(); + bench_head_read_creation(bencher, repeats, &head); +} + +#[divan::bench(args = [1usize, 10, 100])] +fn owned_head_read_creation(bencher: Bencher, repeats: usize) { + let map = zipper_head_fixture(); + let head = black_box(map).into_zipper_head([]); + bench_head_read_creation(bencher, repeats, &head); +} + +#[divan::bench(args = [1usize, 10, 100])] +fn borrowed_head_write_creation_cleanup(bencher: Bencher, repeats: usize) { + let mut map = zipper_head_fixture(); + let head = black_box(&mut map).zipper_head(); + bench_head_write_creation_cleanup(bencher, repeats, &head); +} + +#[divan::bench(args = [1usize, 10, 100])] +fn owned_head_write_creation_cleanup(bencher: Bencher, repeats: usize) { + let map = zipper_head_fixture(); + let head = black_box(map).into_zipper_head([]); + bench_head_write_creation_cleanup(bencher, repeats, &head); +} diff --git a/src/zipper_head.rs b/src/zipper_head.rs index 6623343..f9dc38a 100644 --- a/src/zipper_head.rs +++ b/src/zipper_head.rs @@ -155,11 +155,9 @@ crate::impl_name_only_debug!( /// `ZipperHeadOwned` is useful when managing the lifetime of an ordinary `ZipperHead` is unwieldy, as /// often occurs in multi-threaded situations. /// -/// TODO: `ZipperHeadOwned` should be benchmarked against an ordinary `ZipperHead` to see how much -/// performance is lost. There is a `Mutex` in `ZipperHeadOwned` so that it can be `Sync`, while the -/// ordinary `ZipperHead` uses an `UnsafeCell`. However in a scenario where all the zipper-creation -/// activity was happening from the same thread, it's unclear how much cost in involved locking an -/// unlocking the mutex. +/// Benchmark note: `benches/zipper_head_owned.rs` compares same-thread read creation and write +/// creation/cleanup against ordinary `ZipperHead`. The owned head pays for a `Mutex` so it can be +/// `Sync`, while ordinary `ZipperHead` uses an `UnsafeCell`. pub struct ZipperHeadOwned { z: std::sync::Mutex>, tracker_paths: SharedTrackerPaths,