From 2056262830b81cbfee09b110812a9b817a03a0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= Date: Tue, 26 May 2026 21:42:58 -0400 Subject: [PATCH 01/10] fix: feature gate gungraun --- crates/multitude/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/multitude/Cargo.toml b/crates/multitude/Cargo.toml index 2c05ceef5..67141cbbb 100644 --- a/crates/multitude/Cargo.toml +++ b/crates/multitude/Cargo.toml @@ -47,6 +47,9 @@ bytes = ["dep:bytes"] bytemuck = ["dep:bytemuck", "dst"] bytesbuf = ["dep:bytesbuf", "std"] zerocopy = ["dep:zerocopy", "dst"] +# Internal feature: gates gungraun (Valgrind-based) benchmarks that require +# callgrind to be installed. Not intended for external use. +_gungraun = [] [dependencies] allocator-api2 = { workspace = true, features = ["alloc"] } @@ -89,10 +92,12 @@ harness = false [[bench]] name = "gungraun_alloc" harness = false +required-features = ["_gungraun"] [[bench]] name = "gungraun_drop" harness = false +required-features = ["_gungraun"] [[example]] name = "basic" From b8caca1d5d4557da6d3a6dcc65de53085cbd5edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= Date: Wed, 27 May 2026 11:47:17 -0400 Subject: [PATCH 02/10] refactoring --- constants.env | 1 + crates/multitude/Cargo.toml | 15 +- crates/multitude/benches/gungraun_alloc.rs | 693 +----------------- .../multitude/benches/gungraun_alloc/linux.rs | 670 +++++++++++++++++ crates/multitude/benches/gungraun_drop.rs | 309 +------- .../multitude/benches/gungraun_drop/linux.rs | 278 +++++++ justfiles/setup.just | 3 + scripts/install-callgrind-tools.ps1 | 24 + 8 files changed, 1033 insertions(+), 960 deletions(-) create mode 100644 crates/multitude/benches/gungraun_alloc/linux.rs create mode 100644 crates/multitude/benches/gungraun_drop/linux.rs create mode 100644 scripts/install-callgrind-tools.ps1 diff --git a/constants.env b/constants.env index 8b291bc3d..b57c76d35 100644 --- a/constants.env +++ b/constants.env @@ -35,5 +35,6 @@ CARGO_UDEPS_VERSION=0.1.60 CARGO_WORKSPACES_VERSION=0.4.2 # Other tools +GUNGRAUN_RUNNER_VERSION=0.19.0 JUST_VERSION=1.46.0 SCCACHE_VERSION=v0.13.0 diff --git a/crates/multitude/Cargo.toml b/crates/multitude/Cargo.toml index 67141cbbb..386b819e1 100644 --- a/crates/multitude/Cargo.toml +++ b/crates/multitude/Cargo.toml @@ -47,9 +47,6 @@ bytes = ["dep:bytes"] bytemuck = ["dep:bytemuck", "dst"] bytesbuf = ["dep:bytesbuf", "std"] zerocopy = ["dep:zerocopy", "dst"] -# Internal feature: gates gungraun (Valgrind-based) benchmarks that require -# callgrind to be installed. Not intended for external use. -_gungraun = [] [dependencies] allocator-api2 = { workspace = true, features = ["alloc"] } @@ -74,10 +71,15 @@ bolero = { workspace = true, features = ["std"] } # but does not itself activate that feature on `bolero-engine`. bumpalo = { workspace = true, features = ["collections"] } criterion.workspace = true -gungraun = { workspace = true, features = ["default"] } mutants.workspace = true serde_json.workspace = true +# Workspace dep declares `default-features = false`; we re-enable `default` here +# (which pulls in the `benchmark` feature with `library_benchmark`, +# `library_benchmark_group!`, and `gungraun::main!`) only on Linux. +[target.'cfg(target_os = "linux")'.dev-dependencies] +gungraun = { workspace = true, features = ["default"] } + [lints] workspace = true @@ -89,15 +91,16 @@ harness = false name = "criterion_drop" harness = false +# Callgrind benches require Linux (Valgrind). The bench files are gated to compile +# to a no-op on non-Linux targets, but the [[bench]] entry itself cannot be +# cfg-gated, so it is unconditional here. [[bench]] name = "gungraun_alloc" harness = false -required-features = ["_gungraun"] [[bench]] name = "gungraun_drop" harness = false -required-features = ["_gungraun"] [[example]] name = "basic" diff --git a/crates/multitude/benches/gungraun_alloc.rs b/crates/multitude/benches/gungraun_alloc.rs index 0aed367b6..e98723fd3 100644 --- a/crates/multitude/benches/gungraun_alloc.rs +++ b/crates/multitude/benches/gungraun_alloc.rs @@ -9,7 +9,7 @@ //! //! Run with `cargo bench --bench gungraun_alloc` on a Linux host with Valgrind. -#![expect(missing_docs, reason = "Benchmark")] +#![allow(missing_docs, reason = "Benchmark")] #![allow(unused_results, reason = "black_box of bench input is intentional")] #![allow( clippy::needless_pass_by_value, @@ -17,679 +17,30 @@ )] #![allow(clippy::ref_as_ptr, reason = "trivial pointer cast in bench plumbing")] #![allow(clippy::too_many_lines, reason = "benchmark file")] +#![cfg_attr( + target_os = "linux", + expect( + clippy::exit, + clippy::missing_docs_in_private_items, + unused_qualifications, + reason = "Triggered by Gungraun macro expansion. Upstream tracking issues are pending." + ) +)] -use core::hint::black_box; - -use gungraun::{Callgrind, LibraryBenchmarkConfig, library_benchmark, library_benchmark_group, main}; -use multitude::Arena; - -const N: usize = 1_000; -const SLICE_LEN: usize = 8; - -// ===== setup helpers ===== - -fn setup_bumpalo() -> bumpalo::Bump { - // Warm: force first-chunk allocation so the timed region exercises - // only the warm-path bump cursor, not the cold-create cliff. - let bump = bumpalo::Bump::with_capacity(64 * 1024); - let _: &mut u64 = bump.alloc(0_u64); - bump -} - -fn setup_multitude() -> Arena { - // Warm: preallocate one chunk of the largest size class for each - // flavor so the timed region exercises only the warm-path bump - // cursor, not the cold-create cliff. Both flavors are seeded - // because gungraun runs Arc-flavor benches (`alloc_arc`, - // `alloc_*_arc`, `alloc_slice_*_arc`) against the same shared - // setup; without `with_capacity_shared`, those benches would pay - // for a fresh shared chunk allocation on the first iteration. - Arena::builder() - .with_capacity_local(64 * 1024) - .with_capacity_shared(64 * 1024) - .build() -} - -fn setup_word_inputs() -> Vec { - (0..N).map(|i| format!("word{i}")).collect() -} - -fn setup_int_inputs() -> Vec { - (0..N).map(|i| i32::try_from(i).unwrap_or(0)).collect() -} - -fn setup_slice_inputs() -> Vec<[u64; SLICE_LEN]> { - (0..N) - .map(|i| { - let base = i as u64; - [base, base + 1, base + 2, base + 3, base + 4, base + 5, base + 6, base + 7] - }) - .collect() -} - -// ===== alloc_u64: single-value allocation of u64 ===== - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc(arena: Arena) -> Arena { - for i in 0..N { - let _: &mut u64 = black_box(black_box(&arena).alloc(black_box(i as u64))); - } - arena -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_with(arena: Arena) -> Arena { - for i in 0..N { - let _: &mut u64 = black_box(black_box(&arena).alloc_with(|| black_box(i as u64))); - } - arena -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_box(arena: Arena) -> (Vec>, Arena) { - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(black_box(arena.alloc_box(black_box(i as u64)))); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_box_with(arena: Arena) -> (Vec>, Arena) { - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(black_box(arena.alloc_box_with(|| black_box(i as u64)))); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_uninit_box(arena: Arena) -> (Vec>>, Arena) { - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(black_box(arena.alloc_uninit_box::())); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_zeroed_box(arena: Arena) -> (Vec>>, Arena) { - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(black_box(arena.alloc_zeroed_box::())); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_rc(arena: Arena) -> (Vec>, Arena) { - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(black_box(arena.alloc_rc(black_box(i as u64)))); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_rc_with(arena: Arena) -> (Vec>, Arena) { - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(black_box(arena.alloc_rc_with(|| black_box(i as u64)))); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_uninit_rc(arena: Arena) -> (Vec>>, Arena) { - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(black_box(arena.alloc_uninit_rc::())); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_zeroed_rc(arena: Arena) -> (Vec>>, Arena) { - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(black_box(arena.alloc_zeroed_rc::())); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_arc(arena: Arena) -> (Vec>, Arena) { - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(black_box(arena.alloc_arc(black_box(i as u64)))); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_arc_with(arena: Arena) -> (Vec>, Arena) { - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(black_box(arena.alloc_arc_with(|| black_box(i as u64)))); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_uninit_arc(arena: Arena) -> (Vec>>, Arena) { - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(black_box(arena.alloc_uninit_arc::())); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_zeroed_arc(arena: Arena) -> (Vec>>, Arena) { - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(black_box(arena.alloc_zeroed_arc::())); - } - (h, arena) -} - -#[library_benchmark] -#[bench::run(setup_bumpalo())] -fn alloc_u64_bumpalo(bump: bumpalo::Bump) -> bumpalo::Bump { - for i in 0..N { - let _: &mut u64 = black_box(black_box(&bump).alloc(black_box(i as u64))); - } - bump -} - -#[library_benchmark] -#[bench::run(setup_bumpalo())] -fn alloc_u64_bumpalo_with(bump: bumpalo::Bump) -> bumpalo::Bump { - for i in 0..N { - let _: &mut u64 = black_box(black_box(&bump).alloc_with(|| black_box(i as u64))); - } - bump -} - -// ===== alloc_str: single &str allocation ===== - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_word_inputs())] -fn alloc_str(arena: Arena, words: Vec) -> (Vec<*mut str>, Arena) { - let mut out: Vec<*mut str> = Vec::with_capacity(N); - for w in &words { - let s: &mut str = black_box(arena.alloc_str(black_box(w))); - out.push(s as *mut str); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_word_inputs())] -fn alloc_str_box(arena: Arena, words: Vec) -> (Vec, Arena) { - let mut out = Vec::with_capacity(N); - for w in &words { - out.push(black_box(arena.alloc_str_box(black_box(w)))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_word_inputs())] -fn alloc_str_rc(arena: Arena, words: Vec) -> (Vec, Arena) { - let mut out = Vec::with_capacity(N); - for w in &words { - out.push(black_box(arena.alloc_str_rc(black_box(w)))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_word_inputs())] -fn alloc_str_arc(arena: Arena, words: Vec) -> (Vec, Arena) { - let mut out = Vec::with_capacity(N); - for w in &words { - out.push(black_box(arena.alloc_str_arc(black_box(w)))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_bumpalo(), setup_word_inputs())] -fn alloc_str_bumpalo(bump: bumpalo::Bump, words: Vec) -> (Vec<*mut str>, bumpalo::Bump) { - let mut out: Vec<*mut str> = Vec::with_capacity(N); - for w in &words { - let s: &mut str = black_box(black_box(&bump).alloc_str(black_box(w))); - out.push(s as *mut str); - } - (out, bump) -} - -// ===== alloc_slice: slice, len = SLICE_LEN, N batches ===== - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_slice_inputs())] -fn alloc_slice_copy(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> Arena { - for s in &slices { - let _: &mut [u64] = black_box(arena.alloc_slice_copy(black_box(s))); - } - arena -} - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_slice_inputs())] -fn alloc_slice_clone(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> Arena { - for s in &slices { - let _: &mut [u64] = black_box(arena.alloc_slice_clone(black_box(s.as_slice()))); - } - arena -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_slice_fill_with(arena: Arena) -> Arena { - for _ in 0..N { - let _: &mut [u64] = black_box(arena.alloc_slice_fill_with::(SLICE_LEN, |j| black_box(j as u64))); - } - arena -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_slice_fill_iter(arena: Arena) -> Arena { - for _ in 0..N { - let _: &mut [u64] = black_box(arena.alloc_slice_fill_iter((0..SLICE_LEN).map(|j| black_box(j as u64)))); - } - arena -} - -// box variants -#[library_benchmark] -#[bench::run(setup_multitude(), setup_slice_inputs())] -fn alloc_slice_copy_box(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for s in &slices { - out.push(black_box(arena.alloc_slice_copy_box(black_box(s)))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_slice_inputs())] -fn alloc_slice_clone_box(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for s in &slices { - out.push(black_box(arena.alloc_slice_clone_box(black_box(s.as_slice())))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_slice_fill_with_box(arena: Arena) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box( - arena.alloc_slice_fill_with_box::(SLICE_LEN, |j| black_box(j as u64)), - )); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_slice_fill_iter_box(arena: Arena) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box( - arena.alloc_slice_fill_iter_box((0..SLICE_LEN).map(|j| black_box(j as u64))), - )); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_uninit_slice_box(arena: Arena) -> (Vec]>>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box(arena.alloc_uninit_slice_box::(SLICE_LEN))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_zeroed_slice_box(arena: Arena) -> (Vec]>>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box(arena.alloc_zeroed_slice_box::(SLICE_LEN))); - } - (out, arena) -} - -// rc variants -#[library_benchmark] -#[bench::run(setup_multitude(), setup_slice_inputs())] -fn alloc_slice_copy_rc(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for s in &slices { - out.push(black_box(arena.alloc_slice_copy_rc(black_box(s)))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_slice_inputs())] -fn alloc_slice_clone_rc(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for s in &slices { - out.push(black_box(arena.alloc_slice_clone_rc(black_box(s.as_slice())))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_slice_fill_with_rc(arena: Arena) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box( - arena.alloc_slice_fill_with_rc::(SLICE_LEN, |j| black_box(j as u64)), - )); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_slice_fill_iter_rc(arena: Arena) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box( - arena.alloc_slice_fill_iter_rc((0..SLICE_LEN).map(|j| black_box(j as u64))), - )); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_uninit_slice_rc(arena: Arena) -> (Vec]>>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box(arena.alloc_uninit_slice_rc::(SLICE_LEN))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_zeroed_slice_rc(arena: Arena) -> (Vec]>>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box(arena.alloc_zeroed_slice_rc::(SLICE_LEN))); - } - (out, arena) -} - -// arc variants -#[library_benchmark] -#[bench::run(setup_multitude(), setup_slice_inputs())] -fn alloc_slice_copy_arc(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for s in &slices { - out.push(black_box(arena.alloc_slice_copy_arc(black_box(s)))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_slice_inputs())] -fn alloc_slice_clone_arc(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for s in &slices { - out.push(black_box(arena.alloc_slice_clone_arc(black_box(s.as_slice())))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_slice_fill_with_arc(arena: Arena) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box( - arena.alloc_slice_fill_with_arc::(SLICE_LEN, |j| black_box(j as u64)), - )); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_slice_fill_iter_arc(arena: Arena) -> (Vec>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box( - arena.alloc_slice_fill_iter_arc((0..SLICE_LEN).map(|j| black_box(j as u64))), - )); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_uninit_slice_arc(arena: Arena) -> (Vec]>>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box(arena.alloc_uninit_slice_arc::(SLICE_LEN))); - } - (out, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude())] -fn alloc_zeroed_slice_arc(arena: Arena) -> (Vec]>>, Arena) { - let mut out = Vec::with_capacity(N); - for _ in 0..N { - out.push(black_box(arena.alloc_zeroed_slice_arc::(SLICE_LEN))); - } - (out, arena) -} - -// bumpalo slice variants -#[library_benchmark] -#[bench::run(setup_bumpalo(), setup_slice_inputs())] -fn alloc_slice_bumpalo_copy(bump: bumpalo::Bump, slices: Vec<[u64; SLICE_LEN]>) -> bumpalo::Bump { - for s in &slices { - let _: &mut [u64] = black_box(black_box(&bump).alloc_slice_copy(black_box(s.as_slice()))); - } - bump -} - -#[library_benchmark] -#[bench::run(setup_bumpalo(), setup_slice_inputs())] -fn alloc_slice_bumpalo_clone(bump: bumpalo::Bump, slices: Vec<[u64; SLICE_LEN]>) -> bumpalo::Bump { - for s in &slices { - let _: &mut [u64] = black_box(black_box(&bump).alloc_slice_clone(black_box(s.as_slice()))); - } - bump -} - -#[library_benchmark] -#[bench::run(setup_bumpalo())] -fn alloc_slice_bumpalo_fill_with(bump: bumpalo::Bump) -> bumpalo::Bump { - for _ in 0..N { - let _: &mut [u64] = black_box(black_box(&bump).alloc_slice_fill_with::(SLICE_LEN, |j| black_box(j as u64))); - } - bump -} - -#[library_benchmark] -#[bench::run(setup_bumpalo())] -fn alloc_slice_bumpalo_fill_iter(bump: bumpalo::Bump) -> bumpalo::Bump { - for _ in 0..N { - let _: &mut [u64] = black_box(black_box(&bump).alloc_slice_fill_iter((0..SLICE_LEN).map(|j| black_box(j as u64)))); - } - bump -} - -// ===== string_builder: push N tokens, freeze ===== - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_word_inputs())] -fn alloc_string(arena: Arena, words: Vec) -> (multitude::strings::RcStr, Arena) { - let mut s = arena.alloc_string(); - for w in &words { - s.push_str(black_box(w.as_str())); - } - let frozen = black_box(s.into_arena_str()); - (frozen, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_word_inputs())] -fn alloc_string_with_capacity(arena: Arena, words: Vec) -> (multitude::strings::RcStr, Arena) { - let mut s = arena.alloc_string_with_capacity(N * 6); - for w in &words { - s.push_str(black_box(w.as_str())); - } - let frozen = black_box(s.into_arena_str()); - (frozen, arena) -} - -#[library_benchmark] -#[bench::run(setup_bumpalo(), setup_word_inputs())] -fn string_builder_bumpalo_grow(bump: bumpalo::Bump, words: Vec) -> (*const str, bumpalo::Bump) { - let mut s = bumpalo::collections::String::new_in(&bump); - for w in &words { - s.push_str(black_box(w.as_str())); - } - let frozen: &str = black_box(s.into_bump_str()); - (frozen as *const str, bump) -} - -#[library_benchmark] -#[bench::run(setup_bumpalo(), setup_word_inputs())] -fn string_builder_bumpalo_with_cap(bump: bumpalo::Bump, words: Vec) -> (*const str, bumpalo::Bump) { - let mut s = bumpalo::collections::String::with_capacity_in(N * 6, &bump); - for w in &words { - s.push_str(black_box(w.as_str())); - } - let frozen: &str = black_box(s.into_bump_str()); - (frozen as *const str, bump) -} - -// ===== vec_builder: push N i32, freeze ===== - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_int_inputs())] -fn alloc_vec(arena: Arena, ints: Vec) -> (multitude::Rc<[i32]>, Arena) { - let mut v = arena.alloc_vec::(); - for &i in &ints { - v.push(black_box(i)); - } - let frozen = black_box(v.into_arena_rc()); - (frozen, arena) -} - -#[library_benchmark] -#[bench::run(setup_multitude(), setup_int_inputs())] -fn alloc_vec_with_capacity(arena: Arena, ints: Vec) -> (multitude::Rc<[i32]>, Arena) { - let mut v = arena.alloc_vec_with_capacity::(N); - for &i in &ints { - v.push(black_box(i)); - } - let frozen = black_box(v.into_arena_rc()); - (frozen, arena) -} - -#[library_benchmark] -#[bench::run(setup_bumpalo(), setup_int_inputs())] -fn vec_builder_bumpalo_grow(bump: bumpalo::Bump, ints: Vec) -> (*const [i32], bumpalo::Bump) { - let mut v: bumpalo::collections::Vec<'_, i32> = bumpalo::collections::Vec::new_in(&bump); - for &i in &ints { - v.push(black_box(i)); - } - let frozen: &[i32] = black_box(v.into_bump_slice()); - (frozen as *const [i32], bump) -} - -#[library_benchmark] -#[bench::run(setup_bumpalo(), setup_int_inputs())] -fn vec_builder_bumpalo_with_cap(bump: bumpalo::Bump, ints: Vec) -> (*const [i32], bumpalo::Bump) { - let mut v: bumpalo::collections::Vec<'_, i32> = bumpalo::collections::Vec::with_capacity_in(N, &bump); - for &i in &ints { - v.push(black_box(i)); - } - let frozen: &[i32] = black_box(v.into_bump_slice()); - (frozen as *const [i32], bump) -} - -// ===== arena_creation: standalone Arena/Bump construction + drop ===== - -#[library_benchmark] -fn arena_creation_multitude() { - let arena = black_box(Arena::new()); - drop(arena); -} +// Gungraun requires Valgrind, which is Linux-only. On other platforms this +// bench target compiles to a no-op so `cargo build --all-targets` still works. +#[cfg(not(target_os = "linux"))] +fn main() {} -#[library_benchmark] -fn arena_creation_bumpalo() { - let bump = black_box(bumpalo::Bump::new()); - drop(bump); -} +#[cfg(target_os = "linux")] +mod linux; -library_benchmark_group!( - name = alloc_group; - benchmarks = - arena_creation_multitude, arena_creation_bumpalo, - alloc, alloc_with, - alloc_box, alloc_box_with, - alloc_uninit_box, alloc_zeroed_box, - alloc_rc, alloc_rc_with, - alloc_uninit_rc, alloc_zeroed_rc, - alloc_arc, alloc_arc_with, - alloc_uninit_arc, alloc_zeroed_arc, - alloc_u64_bumpalo, alloc_u64_bumpalo_with, - alloc_str, alloc_str_box, - alloc_str_rc, alloc_str_arc, alloc_str_bumpalo, - alloc_slice_copy, alloc_slice_clone, - alloc_slice_fill_with, alloc_slice_fill_iter, - alloc_slice_copy_box, alloc_slice_clone_box, - alloc_slice_fill_with_box, alloc_slice_fill_iter_box, - alloc_uninit_slice_box, alloc_zeroed_slice_box, - alloc_slice_copy_rc, alloc_slice_clone_rc, - alloc_slice_fill_with_rc, alloc_slice_fill_iter_rc, - alloc_uninit_slice_rc, alloc_zeroed_slice_rc, - alloc_slice_copy_arc, alloc_slice_clone_arc, - alloc_slice_fill_with_arc, alloc_slice_fill_iter_arc, - alloc_uninit_slice_arc, alloc_zeroed_slice_arc, - alloc_slice_bumpalo_copy, alloc_slice_bumpalo_clone, - alloc_slice_bumpalo_fill_with, alloc_slice_bumpalo_fill_iter, - alloc_string, alloc_string_with_capacity, - string_builder_bumpalo_grow, string_builder_bumpalo_with_cap, - alloc_vec, alloc_vec_with_capacity, - vec_builder_bumpalo_grow, vec_builder_bumpalo_with_cap -); +#[cfg(target_os = "linux")] +use linux::*; -main!( - config = LibraryBenchmarkConfig::default() - .tool(Callgrind::with_args(["--branch-sim=yes"])); +#[cfg(target_os = "linux")] +gungraun::main!( + config = gungraun::LibraryBenchmarkConfig::default() + .tool(gungraun::Callgrind::with_args(["--branch-sim=yes"])); library_benchmark_groups = alloc_group ); diff --git a/crates/multitude/benches/gungraun_alloc/linux.rs b/crates/multitude/benches/gungraun_alloc/linux.rs new file mode 100644 index 000000000..7f8225950 --- /dev/null +++ b/crates/multitude/benches/gungraun_alloc/linux.rs @@ -0,0 +1,670 @@ +use core::hint::black_box; + +use gungraun::{library_benchmark, library_benchmark_group}; +use multitude::Arena; + +const N: usize = 1_000; +const SLICE_LEN: usize = 8; + +// ===== setup helpers ===== + +fn setup_bumpalo() -> bumpalo::Bump { + // Warm: force first-chunk allocation so the timed region exercises + // only the warm-path bump cursor, not the cold-create cliff. + let bump = bumpalo::Bump::with_capacity(64 * 1024); + let _: &mut u64 = bump.alloc(0_u64); + bump +} + +fn setup_multitude() -> Arena { + // Warm: preallocate one chunk of the largest size class for each + // flavor so the timed region exercises only the warm-path bump + // cursor, not the cold-create cliff. Both flavors are seeded + // because gungraun runs Arc-flavor benches (`alloc_arc`, + // `alloc_*_arc`, `alloc_slice_*_arc`) against the same shared + // setup; without `with_capacity_shared`, those benches would pay + // for a fresh shared chunk allocation on the first iteration. + Arena::builder() + .with_capacity_local(64 * 1024) + .with_capacity_shared(64 * 1024) + .build() +} + +fn setup_word_inputs() -> Vec { + (0..N).map(|i| format!("word{i}")).collect() +} + +fn setup_int_inputs() -> Vec { + (0..N).map(|i| i32::try_from(i).unwrap_or(0)).collect() +} + +fn setup_slice_inputs() -> Vec<[u64; SLICE_LEN]> { + (0..N) + .map(|i| { + let base = i as u64; + [base, base + 1, base + 2, base + 3, base + 4, base + 5, base + 6, base + 7] + }) + .collect() +} + +// ===== alloc_u64: single-value allocation of u64 ===== + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc(arena: Arena) -> Arena { + for i in 0..N { + let _: &mut u64 = black_box(black_box(&arena).alloc(black_box(i as u64))); + } + arena +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_with(arena: Arena) -> Arena { + for i in 0..N { + let _: &mut u64 = black_box(black_box(&arena).alloc_with(|| black_box(i as u64))); + } + arena +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_box(arena: Arena) -> (Vec>, Arena) { + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(black_box(arena.alloc_box(black_box(i as u64)))); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_box_with(arena: Arena) -> (Vec>, Arena) { + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(black_box(arena.alloc_box_with(|| black_box(i as u64)))); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_uninit_box(arena: Arena) -> (Vec>>, Arena) { + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(black_box(arena.alloc_uninit_box::())); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_zeroed_box(arena: Arena) -> (Vec>>, Arena) { + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(black_box(arena.alloc_zeroed_box::())); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_rc(arena: Arena) -> (Vec>, Arena) { + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(black_box(arena.alloc_rc(black_box(i as u64)))); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_rc_with(arena: Arena) -> (Vec>, Arena) { + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(black_box(arena.alloc_rc_with(|| black_box(i as u64)))); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_uninit_rc(arena: Arena) -> (Vec>>, Arena) { + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(black_box(arena.alloc_uninit_rc::())); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_zeroed_rc(arena: Arena) -> (Vec>>, Arena) { + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(black_box(arena.alloc_zeroed_rc::())); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_arc(arena: Arena) -> (Vec>, Arena) { + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(black_box(arena.alloc_arc(black_box(i as u64)))); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_arc_with(arena: Arena) -> (Vec>, Arena) { + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(black_box(arena.alloc_arc_with(|| black_box(i as u64)))); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_uninit_arc(arena: Arena) -> (Vec>>, Arena) { + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(black_box(arena.alloc_uninit_arc::())); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_zeroed_arc(arena: Arena) -> (Vec>>, Arena) { + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(black_box(arena.alloc_zeroed_arc::())); + } + (h, arena) +} + +#[library_benchmark] +#[bench::run(setup_bumpalo())] +fn alloc_u64_bumpalo(bump: bumpalo::Bump) -> bumpalo::Bump { + for i in 0..N { + let _: &mut u64 = black_box(black_box(&bump).alloc(black_box(i as u64))); + } + bump +} + +#[library_benchmark] +#[bench::run(setup_bumpalo())] +fn alloc_u64_bumpalo_with(bump: bumpalo::Bump) -> bumpalo::Bump { + for i in 0..N { + let _: &mut u64 = black_box(black_box(&bump).alloc_with(|| black_box(i as u64))); + } + bump +} + +// ===== alloc_str: single &str allocation ===== + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_word_inputs())] +fn alloc_str(arena: Arena, words: Vec) -> (Vec<*mut str>, Arena) { + let mut out: Vec<*mut str> = Vec::with_capacity(N); + for w in &words { + let s: &mut str = black_box(arena.alloc_str(black_box(w))); + out.push(s as *mut str); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_word_inputs())] +fn alloc_str_box(arena: Arena, words: Vec) -> (Vec, Arena) { + let mut out = Vec::with_capacity(N); + for w in &words { + out.push(black_box(arena.alloc_str_box(black_box(w)))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_word_inputs())] +fn alloc_str_rc(arena: Arena, words: Vec) -> (Vec, Arena) { + let mut out = Vec::with_capacity(N); + for w in &words { + out.push(black_box(arena.alloc_str_rc(black_box(w)))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_word_inputs())] +fn alloc_str_arc(arena: Arena, words: Vec) -> (Vec, Arena) { + let mut out = Vec::with_capacity(N); + for w in &words { + out.push(black_box(arena.alloc_str_arc(black_box(w)))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_bumpalo(), setup_word_inputs())] +fn alloc_str_bumpalo(bump: bumpalo::Bump, words: Vec) -> (Vec<*mut str>, bumpalo::Bump) { + let mut out: Vec<*mut str> = Vec::with_capacity(N); + for w in &words { + let s: &mut str = black_box(black_box(&bump).alloc_str(black_box(w))); + out.push(s as *mut str); + } + (out, bump) +} + +// ===== alloc_slice: slice, len = SLICE_LEN, N batches ===== + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_slice_inputs())] +fn alloc_slice_copy(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> Arena { + for s in &slices { + let _: &mut [u64] = black_box(arena.alloc_slice_copy(black_box(s))); + } + arena +} + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_slice_inputs())] +fn alloc_slice_clone(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> Arena { + for s in &slices { + let _: &mut [u64] = black_box(arena.alloc_slice_clone(black_box(s.as_slice()))); + } + arena +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_slice_fill_with(arena: Arena) -> Arena { + for _ in 0..N { + let _: &mut [u64] = black_box(arena.alloc_slice_fill_with::(SLICE_LEN, |j| black_box(j as u64))); + } + arena +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_slice_fill_iter(arena: Arena) -> Arena { + for _ in 0..N { + let _: &mut [u64] = black_box(arena.alloc_slice_fill_iter((0..SLICE_LEN).map(|j| black_box(j as u64)))); + } + arena +} + +// box variants +#[library_benchmark] +#[bench::run(setup_multitude(), setup_slice_inputs())] +fn alloc_slice_copy_box(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for s in &slices { + out.push(black_box(arena.alloc_slice_copy_box(black_box(s)))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_slice_inputs())] +fn alloc_slice_clone_box(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for s in &slices { + out.push(black_box(arena.alloc_slice_clone_box(black_box(s.as_slice())))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_slice_fill_with_box(arena: Arena) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box( + arena.alloc_slice_fill_with_box::(SLICE_LEN, |j| black_box(j as u64)), + )); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_slice_fill_iter_box(arena: Arena) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box( + arena.alloc_slice_fill_iter_box((0..SLICE_LEN).map(|j| black_box(j as u64))), + )); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_uninit_slice_box(arena: Arena) -> (Vec]>>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box(arena.alloc_uninit_slice_box::(SLICE_LEN))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_zeroed_slice_box(arena: Arena) -> (Vec]>>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box(arena.alloc_zeroed_slice_box::(SLICE_LEN))); + } + (out, arena) +} + +// rc variants +#[library_benchmark] +#[bench::run(setup_multitude(), setup_slice_inputs())] +fn alloc_slice_copy_rc(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for s in &slices { + out.push(black_box(arena.alloc_slice_copy_rc(black_box(s)))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_slice_inputs())] +fn alloc_slice_clone_rc(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for s in &slices { + out.push(black_box(arena.alloc_slice_clone_rc(black_box(s.as_slice())))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_slice_fill_with_rc(arena: Arena) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box( + arena.alloc_slice_fill_with_rc::(SLICE_LEN, |j| black_box(j as u64)), + )); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_slice_fill_iter_rc(arena: Arena) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box( + arena.alloc_slice_fill_iter_rc((0..SLICE_LEN).map(|j| black_box(j as u64))), + )); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_uninit_slice_rc(arena: Arena) -> (Vec]>>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box(arena.alloc_uninit_slice_rc::(SLICE_LEN))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_zeroed_slice_rc(arena: Arena) -> (Vec]>>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box(arena.alloc_zeroed_slice_rc::(SLICE_LEN))); + } + (out, arena) +} + +// arc variants +#[library_benchmark] +#[bench::run(setup_multitude(), setup_slice_inputs())] +fn alloc_slice_copy_arc(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for s in &slices { + out.push(black_box(arena.alloc_slice_copy_arc(black_box(s)))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_slice_inputs())] +fn alloc_slice_clone_arc(arena: Arena, slices: Vec<[u64; SLICE_LEN]>) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for s in &slices { + out.push(black_box(arena.alloc_slice_clone_arc(black_box(s.as_slice())))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_slice_fill_with_arc(arena: Arena) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box( + arena.alloc_slice_fill_with_arc::(SLICE_LEN, |j| black_box(j as u64)), + )); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_slice_fill_iter_arc(arena: Arena) -> (Vec>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box( + arena.alloc_slice_fill_iter_arc((0..SLICE_LEN).map(|j| black_box(j as u64))), + )); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_uninit_slice_arc(arena: Arena) -> (Vec]>>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box(arena.alloc_uninit_slice_arc::(SLICE_LEN))); + } + (out, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude())] +fn alloc_zeroed_slice_arc(arena: Arena) -> (Vec]>>, Arena) { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + out.push(black_box(arena.alloc_zeroed_slice_arc::(SLICE_LEN))); + } + (out, arena) +} + +// bumpalo slice variants +#[library_benchmark] +#[bench::run(setup_bumpalo(), setup_slice_inputs())] +fn alloc_slice_bumpalo_copy(bump: bumpalo::Bump, slices: Vec<[u64; SLICE_LEN]>) -> bumpalo::Bump { + for s in &slices { + let _: &mut [u64] = black_box(black_box(&bump).alloc_slice_copy(black_box(s.as_slice()))); + } + bump +} + +#[library_benchmark] +#[bench::run(setup_bumpalo(), setup_slice_inputs())] +fn alloc_slice_bumpalo_clone(bump: bumpalo::Bump, slices: Vec<[u64; SLICE_LEN]>) -> bumpalo::Bump { + for s in &slices { + let _: &mut [u64] = black_box(black_box(&bump).alloc_slice_clone(black_box(s.as_slice()))); + } + bump +} + +#[library_benchmark] +#[bench::run(setup_bumpalo())] +fn alloc_slice_bumpalo_fill_with(bump: bumpalo::Bump) -> bumpalo::Bump { + for _ in 0..N { + let _: &mut [u64] = black_box(black_box(&bump).alloc_slice_fill_with::(SLICE_LEN, |j| black_box(j as u64))); + } + bump +} + +#[library_benchmark] +#[bench::run(setup_bumpalo())] +fn alloc_slice_bumpalo_fill_iter(bump: bumpalo::Bump) -> bumpalo::Bump { + for _ in 0..N { + let _: &mut [u64] = black_box(black_box(&bump).alloc_slice_fill_iter((0..SLICE_LEN).map(|j| black_box(j as u64)))); + } + bump +} + +// ===== string_builder: push N tokens, freeze ===== + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_word_inputs())] +fn alloc_string(arena: Arena, words: Vec) -> (multitude::strings::RcStr, Arena) { + let mut s = arena.alloc_string(); + for w in &words { + s.push_str(black_box(w.as_str())); + } + let frozen = black_box(s.into_arena_str()); + (frozen, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_word_inputs())] +fn alloc_string_with_capacity(arena: Arena, words: Vec) -> (multitude::strings::RcStr, Arena) { + let mut s = arena.alloc_string_with_capacity(N * 6); + for w in &words { + s.push_str(black_box(w.as_str())); + } + let frozen = black_box(s.into_arena_str()); + (frozen, arena) +} + +#[library_benchmark] +#[bench::run(setup_bumpalo(), setup_word_inputs())] +fn string_builder_bumpalo_grow(bump: bumpalo::Bump, words: Vec) -> (*const str, bumpalo::Bump) { + let mut s = bumpalo::collections::String::new_in(&bump); + for w in &words { + s.push_str(black_box(w.as_str())); + } + let frozen: &str = black_box(s.into_bump_str()); + (frozen as *const str, bump) +} + +#[library_benchmark] +#[bench::run(setup_bumpalo(), setup_word_inputs())] +fn string_builder_bumpalo_with_cap(bump: bumpalo::Bump, words: Vec) -> (*const str, bumpalo::Bump) { + let mut s = bumpalo::collections::String::with_capacity_in(N * 6, &bump); + for w in &words { + s.push_str(black_box(w.as_str())); + } + let frozen: &str = black_box(s.into_bump_str()); + (frozen as *const str, bump) +} + +// ===== vec_builder: push N i32, freeze ===== + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_int_inputs())] +fn alloc_vec(arena: Arena, ints: Vec) -> (multitude::Rc<[i32]>, Arena) { + let mut v = arena.alloc_vec::(); + for &i in &ints { + v.push(black_box(i)); + } + let frozen = black_box(v.into_arena_rc()); + (frozen, arena) +} + +#[library_benchmark] +#[bench::run(setup_multitude(), setup_int_inputs())] +fn alloc_vec_with_capacity(arena: Arena, ints: Vec) -> (multitude::Rc<[i32]>, Arena) { + let mut v = arena.alloc_vec_with_capacity::(N); + for &i in &ints { + v.push(black_box(i)); + } + let frozen = black_box(v.into_arena_rc()); + (frozen, arena) +} + +#[library_benchmark] +#[bench::run(setup_bumpalo(), setup_int_inputs())] +fn vec_builder_bumpalo_grow(bump: bumpalo::Bump, ints: Vec) -> (*const [i32], bumpalo::Bump) { + let mut v: bumpalo::collections::Vec<'_, i32> = bumpalo::collections::Vec::new_in(&bump); + for &i in &ints { + v.push(black_box(i)); + } + let frozen: &[i32] = black_box(v.into_bump_slice()); + (frozen as *const [i32], bump) +} + +#[library_benchmark] +#[bench::run(setup_bumpalo(), setup_int_inputs())] +fn vec_builder_bumpalo_with_cap(bump: bumpalo::Bump, ints: Vec) -> (*const [i32], bumpalo::Bump) { + let mut v: bumpalo::collections::Vec<'_, i32> = bumpalo::collections::Vec::with_capacity_in(N, &bump); + for &i in &ints { + v.push(black_box(i)); + } + let frozen: &[i32] = black_box(v.into_bump_slice()); + (frozen as *const [i32], bump) +} + +// ===== arena_creation: standalone Arena/Bump construction + drop ===== + +#[library_benchmark] +fn arena_creation_multitude() { + let arena = black_box(Arena::new()); + drop(arena); +} + +#[library_benchmark] +fn arena_creation_bumpalo() { + let bump = black_box(bumpalo::Bump::new()); + drop(bump); +} + +library_benchmark_group!( + name = alloc_group; + benchmarks = + arena_creation_multitude, arena_creation_bumpalo, + alloc, alloc_with, + alloc_box, alloc_box_with, + alloc_uninit_box, alloc_zeroed_box, + alloc_rc, alloc_rc_with, + alloc_uninit_rc, alloc_zeroed_rc, + alloc_arc, alloc_arc_with, + alloc_uninit_arc, alloc_zeroed_arc, + alloc_u64_bumpalo, alloc_u64_bumpalo_with, + alloc_str, alloc_str_box, + alloc_str_rc, alloc_str_arc, alloc_str_bumpalo, + alloc_slice_copy, alloc_slice_clone, + alloc_slice_fill_with, alloc_slice_fill_iter, + alloc_slice_copy_box, alloc_slice_clone_box, + alloc_slice_fill_with_box, alloc_slice_fill_iter_box, + alloc_uninit_slice_box, alloc_zeroed_slice_box, + alloc_slice_copy_rc, alloc_slice_clone_rc, + alloc_slice_fill_with_rc, alloc_slice_fill_iter_rc, + alloc_uninit_slice_rc, alloc_zeroed_slice_rc, + alloc_slice_copy_arc, alloc_slice_clone_arc, + alloc_slice_fill_with_arc, alloc_slice_fill_iter_arc, + alloc_uninit_slice_arc, alloc_zeroed_slice_arc, + alloc_slice_bumpalo_copy, alloc_slice_bumpalo_clone, + alloc_slice_bumpalo_fill_with, alloc_slice_bumpalo_fill_iter, + alloc_string, alloc_string_with_capacity, + string_builder_bumpalo_grow, string_builder_bumpalo_with_cap, + alloc_vec, alloc_vec_with_capacity, + vec_builder_bumpalo_grow, vec_builder_bumpalo_with_cap +); + diff --git a/crates/multitude/benches/gungraun_drop.rs b/crates/multitude/benches/gungraun_drop.rs index e67563321..49dc4026f 100644 --- a/crates/multitude/benches/gungraun_drop.rs +++ b/crates/multitude/benches/gungraun_drop.rs @@ -9,290 +9,33 @@ //! (handle vec + arena), measuring per-handle smart-pointer drop plus chunk //! teardown at arena drop. -#![expect(missing_docs, reason = "Benchmark")] +#![allow(missing_docs, reason = "Benchmark")] #![allow(unused_results, reason = "black_box of bench input is intentional")] #![allow(clippy::too_many_lines, reason = "benchmark file")] - -use core::hint::black_box; - -use gungraun::{Callgrind, LibraryBenchmarkConfig, library_benchmark, library_benchmark_group, main}; -use multitude::strings::{ArcStr, BoxStr, RcStr}; -use multitude::{Arc, Arena, Box, Rc}; - -const N: usize = 1_000; -const SLICE_LEN: usize = 8; - -// `std::Box` is the `T: Drop` test type — its destructor calls into the -// global allocator, exercising the chunk drop-list traversal. -type DroppyT = std::boxed::Box; - -#[expect(clippy::unnecessary_box_returns, reason = "Box is the T: Drop probe")] -fn make_droppy(i: usize) -> DroppyT { - std::boxed::Box::new(i as u64) -} - -// ===== single-value handle drops ===== - -fn setup_box_u64() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(arena.alloc_box(i as u64)); - } - (h, arena) -} - -fn setup_rc_u64() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(arena.alloc_rc(i as u64)); - } - (h, arena) -} - -fn setup_arc_u64() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(arena.alloc_arc(i as u64)); - } - (h, arena) -} - -fn setup_box_droppy() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(arena.alloc_box(make_droppy(i))); - } - (h, arena) -} - -fn setup_rc_droppy() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(arena.alloc_rc(make_droppy(i))); - } - (h, arena) -} - -fn setup_arc_droppy() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(arena.alloc_arc(make_droppy(i))); - } - (h, arena) -} - -// ===== str handle drops ===== - -fn setup_str_box() -> (Vec, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(arena.alloc_str_box(format!("word{i}"))); - } - (h, arena) -} - -fn setup_str_rc() -> (Vec, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(arena.alloc_str_rc(format!("word{i}"))); - } - (h, arena) -} - -fn setup_str_arc() -> (Vec, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for i in 0..N { - h.push(arena.alloc_str_arc(format!("word{i}"))); - } - (h, arena) -} - -// ===== slice handle drops ===== - -fn setup_slice_box_u64() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(arena.alloc_slice_fill_with_box::(SLICE_LEN, |j| j as u64)); - } - (h, arena) -} - -fn setup_slice_rc_u64() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(arena.alloc_slice_fill_with_rc::(SLICE_LEN, |j| j as u64)); - } - (h, arena) -} - -fn setup_slice_arc_u64() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(arena.alloc_slice_fill_with_arc::(SLICE_LEN, |j| j as u64)); - } - (h, arena) -} - -fn setup_slice_box_droppy() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(arena.alloc_slice_fill_with_box::(SLICE_LEN, make_droppy)); - } - (h, arena) -} - -fn setup_slice_rc_droppy() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(arena.alloc_slice_fill_with_rc::(SLICE_LEN, make_droppy)); - } - (h, arena) -} - -fn setup_slice_arc_droppy() -> (Vec>, Arena) { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - let mut h = Vec::with_capacity(N); - for _ in 0..N { - h.push(arena.alloc_slice_fill_with_arc::(SLICE_LEN, make_droppy)); - } - (h, arena) -} - -// ===== arena-only drop (no handles) ===== - -fn setup_alloc() -> Arena { - let arena = Arena::builder().with_capacity_local(64 * 1024).build(); - for i in 0..N { - let _: &mut u64 = arena.alloc(i as u64); - } - arena -} - -// ===== bench bodies — drop happens at scope exit ===== - -#[library_benchmark] -#[bench::run(setup_box_u64())] -fn drop_box_u64(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_rc_u64())] -fn drop_rc_u64(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_arc_u64())] -fn drop_arc_u64(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_box_droppy())] -fn drop_box_droppy(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_rc_droppy())] -fn drop_rc_droppy(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_arc_droppy())] -fn drop_arc_droppy(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_str_box())] -fn drop_str_box(state: (Vec, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_str_rc())] -fn drop_str_rc(state: (Vec, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_str_arc())] -fn drop_str_arc(state: (Vec, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_slice_box_u64())] -fn drop_slice_box_u64(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_slice_rc_u64())] -fn drop_slice_rc_u64(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_slice_arc_u64())] -fn drop_slice_arc_u64(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_slice_box_droppy())] -fn drop_slice_box_droppy(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_slice_rc_droppy())] -fn drop_slice_rc_droppy(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_slice_arc_droppy())] -fn drop_slice_arc_droppy(state: (Vec>, Arena)) { - black_box(state); -} - -#[library_benchmark] -#[bench::run(setup_alloc())] -fn drop_alloc(state: Arena) { - black_box(state); -} - -library_benchmark_group!( - name = drop_group; - benchmarks = - drop_box_u64, drop_rc_u64, drop_arc_u64, - drop_box_droppy, drop_rc_droppy, drop_arc_droppy, - drop_str_box, drop_str_rc, drop_str_arc, - drop_slice_box_u64, drop_slice_rc_u64, drop_slice_arc_u64, - drop_slice_box_droppy, drop_slice_rc_droppy, drop_slice_arc_droppy, - drop_alloc -); - -main!( - config = LibraryBenchmarkConfig::default() - .tool(Callgrind::with_args(["--branch-sim=yes"])); +#![cfg_attr( + target_os = "linux", + expect( + clippy::exit, + clippy::missing_docs_in_private_items, + unused_qualifications, + reason = "Triggered by Gungraun macro expansion. Upstream tracking issues are pending." + ) +)] + +// Gungraun requires Valgrind, which is Linux-only. On other platforms this +// bench target compiles to a no-op so `cargo build --all-targets` still works. +#[cfg(not(target_os = "linux"))] +fn main() {} + +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "linux")] +use linux::*; + +#[cfg(target_os = "linux")] +gungraun::main!( + config = gungraun::LibraryBenchmarkConfig::default() + .tool(gungraun::Callgrind::with_args(["--branch-sim=yes"])); library_benchmark_groups = drop_group ); diff --git a/crates/multitude/benches/gungraun_drop/linux.rs b/crates/multitude/benches/gungraun_drop/linux.rs new file mode 100644 index 000000000..a39ca5096 --- /dev/null +++ b/crates/multitude/benches/gungraun_drop/linux.rs @@ -0,0 +1,278 @@ +use core::hint::black_box; + +use gungraun::{library_benchmark, library_benchmark_group}; +use multitude::strings::{ArcStr, BoxStr, RcStr}; +use multitude::{Arc, Arena, Box, Rc}; + +const N: usize = 1_000; +const SLICE_LEN: usize = 8; + +// `std::Box` is the `T: Drop` test type ΓÇö its destructor calls into the +// global allocator, exercising the chunk drop-list traversal. +type DroppyT = std::boxed::Box; + +#[expect(clippy::unnecessary_box_returns, reason = "Box is the T: Drop probe")] +fn make_droppy(i: usize) -> DroppyT { + std::boxed::Box::new(i as u64) +} + +// ===== single-value handle drops ===== + +fn setup_box_u64() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(arena.alloc_box(i as u64)); + } + (h, arena) +} + +fn setup_rc_u64() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(arena.alloc_rc(i as u64)); + } + (h, arena) +} + +fn setup_arc_u64() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(arena.alloc_arc(i as u64)); + } + (h, arena) +} + +fn setup_box_droppy() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(arena.alloc_box(make_droppy(i))); + } + (h, arena) +} + +fn setup_rc_droppy() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(arena.alloc_rc(make_droppy(i))); + } + (h, arena) +} + +fn setup_arc_droppy() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(arena.alloc_arc(make_droppy(i))); + } + (h, arena) +} + +// ===== str handle drops ===== + +fn setup_str_box() -> (Vec, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(arena.alloc_str_box(format!("word{i}"))); + } + (h, arena) +} + +fn setup_str_rc() -> (Vec, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(arena.alloc_str_rc(format!("word{i}"))); + } + (h, arena) +} + +fn setup_str_arc() -> (Vec, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for i in 0..N { + h.push(arena.alloc_str_arc(format!("word{i}"))); + } + (h, arena) +} + +// ===== slice handle drops ===== + +fn setup_slice_box_u64() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(arena.alloc_slice_fill_with_box::(SLICE_LEN, |j| j as u64)); + } + (h, arena) +} + +fn setup_slice_rc_u64() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(arena.alloc_slice_fill_with_rc::(SLICE_LEN, |j| j as u64)); + } + (h, arena) +} + +fn setup_slice_arc_u64() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(arena.alloc_slice_fill_with_arc::(SLICE_LEN, |j| j as u64)); + } + (h, arena) +} + +fn setup_slice_box_droppy() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(arena.alloc_slice_fill_with_box::(SLICE_LEN, make_droppy)); + } + (h, arena) +} + +fn setup_slice_rc_droppy() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(arena.alloc_slice_fill_with_rc::(SLICE_LEN, make_droppy)); + } + (h, arena) +} + +fn setup_slice_arc_droppy() -> (Vec>, Arena) { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + let mut h = Vec::with_capacity(N); + for _ in 0..N { + h.push(arena.alloc_slice_fill_with_arc::(SLICE_LEN, make_droppy)); + } + (h, arena) +} + +// ===== arena-only drop (no handles) ===== + +fn setup_alloc() -> Arena { + let arena = Arena::builder().with_capacity_local(64 * 1024).build(); + for i in 0..N { + let _: &mut u64 = arena.alloc(i as u64); + } + arena +} + +// ===== bench bodies ΓÇö drop happens at scope exit ===== + +#[library_benchmark] +#[bench::run(setup_box_u64())] +fn drop_box_u64(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_rc_u64())] +fn drop_rc_u64(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_arc_u64())] +fn drop_arc_u64(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_box_droppy())] +fn drop_box_droppy(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_rc_droppy())] +fn drop_rc_droppy(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_arc_droppy())] +fn drop_arc_droppy(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_str_box())] +fn drop_str_box(state: (Vec, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_str_rc())] +fn drop_str_rc(state: (Vec, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_str_arc())] +fn drop_str_arc(state: (Vec, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_slice_box_u64())] +fn drop_slice_box_u64(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_slice_rc_u64())] +fn drop_slice_rc_u64(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_slice_arc_u64())] +fn drop_slice_arc_u64(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_slice_box_droppy())] +fn drop_slice_box_droppy(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_slice_rc_droppy())] +fn drop_slice_rc_droppy(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_slice_arc_droppy())] +fn drop_slice_arc_droppy(state: (Vec>, Arena)) { + black_box(state); +} + +#[library_benchmark] +#[bench::run(setup_alloc())] +fn drop_alloc(state: Arena) { + black_box(state); +} + +library_benchmark_group!( + name = drop_group; + benchmarks = + drop_box_u64, drop_rc_u64, drop_arc_u64, + drop_box_droppy, drop_rc_droppy, drop_arc_droppy, + drop_str_box, drop_str_rc, drop_str_arc, + drop_slice_box_u64, drop_slice_rc_u64, drop_slice_arc_u64, + drop_slice_box_droppy, drop_slice_rc_droppy, drop_slice_arc_droppy, + drop_alloc +); + diff --git a/justfiles/setup.just b/justfiles/setup.just index 7118eee61..8959303f8 100644 --- a/justfiles/setup.just +++ b/justfiles/setup.just @@ -38,3 +38,6 @@ install-tools: # This tool is not well maintained and fails to actually build if using locked dependencies. Okay then. cargo install cargo-spellcheck@$env:CARGO_SPELLCHECK_VERSION + + # Install platform-specific benchmark toolchain (Callgrind, Linux-only). + & (Join-Path $PSScriptRoot ".." "scripts" "install-callgrind-tools.ps1") diff --git a/scripts/install-callgrind-tools.ps1 b/scripts/install-callgrind-tools.ps1 new file mode 100644 index 000000000..87c7f6116 --- /dev/null +++ b/scripts/install-callgrind-tools.ps1 @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Installs the Callgrind benchmark toolchain (gungraun-runner). Linux-only: +# Callgrind requires Valgrind, which is Linux-only. The runner is the helper +# binary that Callgrind bench binaries (built with `harness = false`) hand +# their work off to via an encoded payload. +# +# Keep the version in lockstep with the `gungraun` workspace dep in +# Cargo.toml and the install command shown in docs/callgrind-benchmarks.md. +# `gungraun-runner` enforces strict string equality on the version +# (`gungraun-runner::runner::compare_versions`), so any patch-level drift +# between the library and the runner causes `*_cg` benches to fail at runtime +# with VersionMismatch. + +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $true + +if (-not $IsLinux) { + Write-Host "Callgrind toolchain is Linux-only; skipping." + return +} + +cargo install --locked gungraun-runner --version $env:GUNGRAUN_RUNNER_VERSION From e34eb1d92a5fe8954cfa888d08d10a778004073a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= Date: Wed, 27 May 2026 12:05:02 -0400 Subject: [PATCH 03/10] fixing license headers --- crates/multitude/benches/gungraun_alloc/linux.rs | 3 +++ crates/multitude/benches/gungraun_drop/linux.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/crates/multitude/benches/gungraun_alloc/linux.rs b/crates/multitude/benches/gungraun_alloc/linux.rs index 7f8225950..2b234b4d0 100644 --- a/crates/multitude/benches/gungraun_alloc/linux.rs +++ b/crates/multitude/benches/gungraun_alloc/linux.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use core::hint::black_box; use gungraun::{library_benchmark, library_benchmark_group}; diff --git a/crates/multitude/benches/gungraun_drop/linux.rs b/crates/multitude/benches/gungraun_drop/linux.rs index a39ca5096..79295a195 100644 --- a/crates/multitude/benches/gungraun_drop/linux.rs +++ b/crates/multitude/benches/gungraun_drop/linux.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use core::hint::black_box; use gungraun::{library_benchmark, library_benchmark_group}; From a0854cea2f965c1c2c02d99a85e0e321bdc583e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= Date: Wed, 27 May 2026 12:05:31 -0400 Subject: [PATCH 04/10] fix multitude --- crates/multitude/Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/multitude/Cargo.toml b/crates/multitude/Cargo.toml index 386b819e1..c085bc8cb 100644 --- a/crates/multitude/Cargo.toml +++ b/crates/multitude/Cargo.toml @@ -63,6 +63,12 @@ zerocopy = { workspace = true, optional = true } [target.'cfg(loom)'.dependencies] loom = { workspace = true } +# Workspace dep declares `default-features = false`; we re-enable `default` here +# (which pulls in the `benchmark` feature with `library_benchmark`, +# `library_benchmark_group!`, and `gungraun::main!`) only on Linux. +[target.'cfg(target_os = "linux")'.dev-dependencies] +gungraun = { workspace = true, features = ["default"] } + [dev-dependencies] allocator-api2 = { workspace = true, features = ["alloc"] } bolero = { workspace = true, features = ["std"] } @@ -74,12 +80,6 @@ criterion.workspace = true mutants.workspace = true serde_json.workspace = true -# Workspace dep declares `default-features = false`; we re-enable `default` here -# (which pulls in the `benchmark` feature with `library_benchmark`, -# `library_benchmark_group!`, and `gungraun::main!`) only on Linux. -[target.'cfg(target_os = "linux")'.dev-dependencies] -gungraun = { workspace = true, features = ["default"] } - [lints] workspace = true From 6f1ab70f34535c442a2232cac14ebedb20e76f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= Date: Wed, 27 May 2026 12:08:45 -0400 Subject: [PATCH 05/10] fmt --- crates/multitude/benches/gungraun_alloc/linux.rs | 1 - crates/multitude/benches/gungraun_drop/linux.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/crates/multitude/benches/gungraun_alloc/linux.rs b/crates/multitude/benches/gungraun_alloc/linux.rs index 2b234b4d0..991d1fe3d 100644 --- a/crates/multitude/benches/gungraun_alloc/linux.rs +++ b/crates/multitude/benches/gungraun_alloc/linux.rs @@ -670,4 +670,3 @@ library_benchmark_group!( alloc_vec, alloc_vec_with_capacity, vec_builder_bumpalo_grow, vec_builder_bumpalo_with_cap ); - diff --git a/crates/multitude/benches/gungraun_drop/linux.rs b/crates/multitude/benches/gungraun_drop/linux.rs index 79295a195..6c8505a6b 100644 --- a/crates/multitude/benches/gungraun_drop/linux.rs +++ b/crates/multitude/benches/gungraun_drop/linux.rs @@ -278,4 +278,3 @@ library_benchmark_group!( drop_slice_box_droppy, drop_slice_rc_droppy, drop_slice_arc_droppy, drop_alloc ); - From 8b8be24f4deb42ad92698722e6287fe6ccf5ac7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= Date: Wed, 27 May 2026 12:44:24 -0400 Subject: [PATCH 06/10] benches as main.rs files --- .../benches/{gungraun_alloc.rs => gungraun_alloc/main.rs} | 0 .../multitude/benches/{gungraun_drop.rs => gungraun_drop/main.rs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename crates/multitude/benches/{gungraun_alloc.rs => gungraun_alloc/main.rs} (100%) rename crates/multitude/benches/{gungraun_drop.rs => gungraun_drop/main.rs} (100%) diff --git a/crates/multitude/benches/gungraun_alloc.rs b/crates/multitude/benches/gungraun_alloc/main.rs similarity index 100% rename from crates/multitude/benches/gungraun_alloc.rs rename to crates/multitude/benches/gungraun_alloc/main.rs diff --git a/crates/multitude/benches/gungraun_drop.rs b/crates/multitude/benches/gungraun_drop/main.rs similarity index 100% rename from crates/multitude/benches/gungraun_drop.rs rename to crates/multitude/benches/gungraun_drop/main.rs From c2391ef1db1b72761c1004665c7ccfc9f625ac48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Wed, 27 May 2026 12:58:40 -0400 Subject: [PATCH 07/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crates/multitude/benches/gungraun_drop/linux.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/multitude/benches/gungraun_drop/linux.rs b/crates/multitude/benches/gungraun_drop/linux.rs index 6c8505a6b..a6dccd94c 100644 --- a/crates/multitude/benches/gungraun_drop/linux.rs +++ b/crates/multitude/benches/gungraun_drop/linux.rs @@ -170,7 +170,7 @@ fn setup_alloc() -> Arena { arena } -// ===== bench bodies ΓÇö drop happens at scope exit ===== +// ===== bench bodies — drop happens at scope exit ===== #[library_benchmark] #[bench::run(setup_box_u64())] From 3e90b8a3445432b09f69ce2ca03faf9521fa2665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Wed, 27 May 2026 12:58:51 -0400 Subject: [PATCH 08/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crates/multitude/benches/gungraun_drop/linux.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/multitude/benches/gungraun_drop/linux.rs b/crates/multitude/benches/gungraun_drop/linux.rs index a6dccd94c..b8c08274f 100644 --- a/crates/multitude/benches/gungraun_drop/linux.rs +++ b/crates/multitude/benches/gungraun_drop/linux.rs @@ -10,7 +10,7 @@ use multitude::{Arc, Arena, Box, Rc}; const N: usize = 1_000; const SLICE_LEN: usize = 8; -// `std::Box` is the `T: Drop` test type ΓÇö its destructor calls into the +// `std::Box` is the `T: Drop` test type — its destructor calls into the // global allocator, exercising the chunk drop-list traversal. type DroppyT = std::boxed::Box; From b7933033cde77d59a71dbf7c1ec5f0a0d8420e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= Date: Wed, 27 May 2026 13:00:21 -0400 Subject: [PATCH 09/10] fix docs --- scripts/install-callgrind-tools.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install-callgrind-tools.ps1 b/scripts/install-callgrind-tools.ps1 index 87c7f6116..201e3f56b 100644 --- a/scripts/install-callgrind-tools.ps1 +++ b/scripts/install-callgrind-tools.ps1 @@ -7,7 +7,7 @@ # their work off to via an encoded payload. # # Keep the version in lockstep with the `gungraun` workspace dep in -# Cargo.toml and the install command shown in docs/callgrind-benchmarks.md. +# Cargo.toml and the constants.env file. # `gungraun-runner` enforces strict string equality on the version # (`gungraun-runner::runner::compare_versions`), so any patch-level drift # between the library and the runner causes `*_cg` benches to fail at runtime From 85fff3d7770811763347327d66ba0b1963010a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= Date: Wed, 27 May 2026 15:51:11 -0400 Subject: [PATCH 10/10] mutants fix --- crates/multitude/src/arena/tests.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/multitude/src/arena/tests.rs b/crates/multitude/src/arena/tests.rs index 93895ec26..3885482d7 100644 --- a/crates/multitude/src/arena/tests.rs +++ b/crates/multitude/src/arena/tests.rs @@ -329,6 +329,27 @@ fn alloc_slice_local_with_or_panic_no_drop_fn_does_not_reserve_drop_entry() { ); } +/// Kills `inner_slice.rs:975:47 && -> ||` mutant in `try_alloc_slice_shared_with`. +/// With the mutation `drop_fn.is_some() || len != 0`, an empty slice of a Drop +/// type would erroneously reserve a drop entry. Verify `drop_back` stays put. +#[test] +fn alloc_slice_shared_with_or_panic_empty_drop_type_does_not_reserve_drop_entry() { + let arena = Arena::::new(); + let _ = arena.alloc_arc(0_u32); + let drop_back_before = arena.current_shared.drop_back.get(); + let raw = arena.alloc_slice_shared_with_or_panic::(0, Some(noop_drop_shim), |_, slot| { + slot.write(0); + }); + let drop_back_after = arena.current_shared.drop_back.get(); + // SAFETY: `raw` was just returned by the shared slice allocator which + // bumped the chunk's smart-pointer refcount by 1 for us. + let _arc: crate::Arc<[u8], Global> = unsafe { crate::Arc::from_value_ptr(raw) }; + assert_eq!( + drop_back_before, drop_back_after, + "drop_back must not retreat for empty slice even with drop_fn" + ); +} + /// Kills `arena.rs:3603:47 && -> ||` mutant — shared-flavor sibling of /// the previous test. #[test]