diff --git a/.spelling b/.spelling
index 96834b58c..27d7c4f13 100644
--- a/.spelling
+++ b/.spelling
@@ -608,3 +608,5 @@ u32
POV
lossy
unrounded
+unpadded
+unyielded
diff --git a/Cargo.lock b/Cargo.lock
index 19cc31de8..2ab85d258 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2370,7 +2370,7 @@ dependencies = [
[[package]]
name = "multitude"
-version = "0.3.1"
+version = "0.3.2"
dependencies = [
"allocator-api2 0.4.0",
"bolero",
diff --git a/Cargo.toml b/Cargo.toml
index c27799b3d..f827117cb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,7 +45,7 @@ fundle_macros = { path = "crates/fundle_macros", default-features = false, versi
fundle_macros_impl = { path = "crates/fundle_macros_impl", default-features = false, version = "0.3.3" }
http_extensions = { path = "crates/http_extensions", default-features = false, version = "0.6.2" }
layered = { path = "crates/layered", default-features = false, version = "0.3.4" }
-multitude = { path = "crates/multitude", default-features = false, version = "0.3.1" }
+multitude = { path = "crates/multitude", default-features = false, version = "0.3.2" }
ohno = { path = "crates/ohno", default-features = false, version = "0.3.6" }
ohno_macros = { path = "crates/ohno_macros", default-features = false, version = "0.3.4" }
recoverable = { path = "crates/recoverable", default-features = false, version = "0.1.6" }
diff --git a/crates/multitude/Cargo.toml b/crates/multitude/Cargo.toml
index cf52a9d36..5ea59ff16 100644
--- a/crates/multitude/Cargo.toml
+++ b/crates/multitude/Cargo.toml
@@ -3,7 +3,7 @@
[package]
name = "multitude"
-version = "0.3.1"
+version = "0.3.2"
description = "Fast and flexible arena allocator."
readme = "README.md"
keywords = ["arena", "memory", "allocator", "bump"]
@@ -91,6 +91,10 @@ harness = false
name = "criterion_drop"
harness = false
+[[bench]]
+name = "criterion_arc_array"
+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.
@@ -102,6 +106,10 @@ harness = false
name = "gungraun_drop"
harness = false
+[[bench]]
+name = "gungraun_arc_array"
+harness = false
+
[[example]]
name = "multitude_basic"
diff --git a/crates/multitude/README.md b/crates/multitude/README.md
index 0e511587e..a728bfa11 100644
--- a/crates/multitude/README.md
+++ b/crates/multitude/README.md
@@ -397,94 +397,94 @@ existing `_arc` slice methods).
This crate was developed as part of The Oxidizer Project. Browse this crate's source code.
- [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQbBzV3ofWgqIgbt8brW1MeN_Mb9N6Ac8XJFEIbIYjmnKUrOjRhZIWCaGJ5dGVtdWNrZjEuMjUuMIJlYnl0ZXNmMS4xMS4xgmhieXRlc2J1ZmUwLjUuNYJpbXVsdGl0dWRlZTAuMy4xgmh6ZXJvY29weWYwLjguNTA
+ [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQbBzV3ofWgqIgbt8brW1MeN_Mb9N6Ac8XJFEIbIYjmnKUrOjRhZIWCaGJ5dGVtdWNrZjEuMjUuMIJlYnl0ZXNmMS4xMS4xgmhieXRlc2J1ZmUwLjUuNYJpbXVsdGl0dWRlZTAuMy4ygmh6ZXJvY29weWYwLjguNTA
[__link0]: https://crates.io/crates/bumpalo
- [__link1]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link10]: https://docs.rs/multitude/0.3.1/multitude/?search=vec::Vec
+ [__link1]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link10]: https://docs.rs/multitude/0.3.2/multitude/?search=vec::Vec
[__link11]: https://crates.io/crates/dst-factory
- [__link12]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::format
- [__link13]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::ArcUtf16Str
- [__link14]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::BoxUtf16Str
- [__link15]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::Utf16String
- [__link16]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::format_utf16
- [__link17]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link18]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link19]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena
- [__link2]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
+ [__link12]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::format
+ [__link13]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::ArcUtf16Str
+ [__link14]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::BoxUtf16Str
+ [__link15]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::Utf16String
+ [__link16]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::format_utf16
+ [__link17]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link18]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link19]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena
+ [__link2]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
[__link20]: https://doc.rust-lang.org/stable/std/marker/trait.Send.html
- [__link21]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link22]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link23]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link24]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
+ [__link21]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link22]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link23]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link24]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
[__link25]: https://doc.rust-lang.org/stable/alloc/?search=boxed::Box
- [__link26]: https://docs.rs/multitude/0.3.1/multitude/?search=vec::Vec
- [__link27]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::String
+ [__link26]: https://docs.rs/multitude/0.3.2/multitude/?search=vec::Vec
+ [__link27]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::String
[__link28]: https://crates.io/crates/allocator-api2
- [__link29]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::String
- [__link3]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link30]: https://docs.rs/multitude/0.3.1/multitude/?search=vec::Vec
- [__link31]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::String::into_boxed_str
- [__link32]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link33]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link34]: https://docs.rs/multitude/0.3.1/multitude/?search=vec::Vec::into_boxed_slice
- [__link35]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link36]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link37]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link38]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link39]: https://docs.rs/multitude/0.3.1/multitude/?search=vec::Vec::leak
- [__link4]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
+ [__link29]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::String
+ [__link3]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link30]: https://docs.rs/multitude/0.3.2/multitude/?search=vec::Vec
+ [__link31]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::String::into_boxed_str
+ [__link32]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link33]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link34]: https://docs.rs/multitude/0.3.2/multitude/?search=vec::Vec::into_boxed_slice
+ [__link35]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link36]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link37]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link38]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link39]: https://docs.rs/multitude/0.3.2/multitude/?search=vec::Vec::leak
+ [__link4]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
[__link40]: https://github.com/microsoft/oxidizer/blob/main/crates/multitude/BUMPALO.md
[__link41]: https://crates.io/crates/bumpalo
- [__link42]: https://docs.rs/multitude/0.3.1/multitude/strings/index.html
- [__link43]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link44]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::ArcUtf16Str
- [__link45]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link46]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::BoxUtf16Str
- [__link47]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena
- [__link48]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::String
- [__link49]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::Utf16String
- [__link5]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link50]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::format
- [__link51]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::format_utf16
- [__link52]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::String
- [__link53]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::String::into_boxed_str
- [__link54]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link55]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::Utf16String
- [__link56]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::Utf16String::into_boxed_utf16_str
- [__link57]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::BoxUtf16Str
- [__link58]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link59]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena
- [__link6]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link60]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena::alloc_dst_arc
- [__link61]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena::alloc_dst_box
+ [__link42]: https://docs.rs/multitude/0.3.2/multitude/strings/index.html
+ [__link43]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link44]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::ArcUtf16Str
+ [__link45]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link46]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::BoxUtf16Str
+ [__link47]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena
+ [__link48]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::String
+ [__link49]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::Utf16String
+ [__link5]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link50]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::format
+ [__link51]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::format_utf16
+ [__link52]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::String
+ [__link53]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::String::into_boxed_str
+ [__link54]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link55]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::Utf16String
+ [__link56]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::Utf16String::into_boxed_utf16_str
+ [__link57]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::BoxUtf16Str
+ [__link58]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link59]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena
+ [__link6]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link60]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena::alloc_dst_arc
+ [__link61]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena::alloc_dst_box
[__link62]: https://doc.rust-lang.org/stable/core/?search=alloc::Layout
[__link63]: https://crates.io/crates/dst-factory
[__link64]: https://doc.rust-lang.org/stable/std/?search=io::Write
- [__link65]: https://docs.rs/multitude/0.3.1/multitude/?search=vec::Vec
- [__link66]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link67]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link68]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::String
- [__link69]: https://docs.rs/multitude/0.3.1/multitude/?search=vec::Vec
- [__link7]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link70]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena::alloc_dst_arc
- [__link71]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena::alloc_dst_box
- [__link72]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::ArcUtf16Str
- [__link73]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::BoxUtf16Str
- [__link74]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::Utf16String
- [__link75]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::format_utf16
+ [__link65]: https://docs.rs/multitude/0.3.2/multitude/?search=vec::Vec
+ [__link66]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link67]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link68]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::String
+ [__link69]: https://docs.rs/multitude/0.3.2/multitude/?search=vec::Vec
+ [__link7]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link70]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena::alloc_dst_arc
+ [__link71]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena::alloc_dst_box
+ [__link72]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::ArcUtf16Str
+ [__link73]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::BoxUtf16Str
+ [__link74]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::Utf16String
+ [__link75]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::format_utf16
[__link76]: https://crates.io/crates/widestring
- [__link77]: https://docs.rs/multitude/0.3.1/multitude/?search=zerocopy::ZerocopyView
+ [__link77]: https://docs.rs/multitude/0.3.2/multitude/?search=zerocopy::ZerocopyView
[__link78]: https://docs.rs/zerocopy/0.8.50/zerocopy/?search=FromZeros
- [__link79]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena::zerocopy
- [__link8]: https://docs.rs/multitude/0.3.1/multitude/?search=Box
- [__link80]: https://docs.rs/multitude/0.3.1/multitude/?search=bytemuck::BytemuckView
+ [__link79]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena::zerocopy
+ [__link8]: https://docs.rs/multitude/0.3.2/multitude/?search=Box
+ [__link80]: https://docs.rs/multitude/0.3.2/multitude/?search=bytemuck::BytemuckView
[__link81]: https://docs.rs/bytemuck/1.25.0/bytemuck/?search=Zeroable
- [__link82]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena::bytemuck
+ [__link82]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena::bytemuck
[__link83]: https://doc.rust-lang.org/stable/std/convert/trait.From.html
- [__link84]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
- [__link85]: https://docs.rs/multitude/0.3.1/multitude/?search=Arc
+ [__link84]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
+ [__link85]: https://docs.rs/multitude/0.3.2/multitude/?search=Arc
[__link86]: https://docs.rs/bytes/1.11.1/bytes/?search=Bytes
[__link87]: https://docs.rs/bytesbuf/0.5.5/bytesbuf/?search=mem::Memory
- [__link88]: https://docs.rs/multitude/0.3.1/multitude/?search=Arena
+ [__link88]: https://docs.rs/multitude/0.3.2/multitude/?search=Arena
[__link89]: https://docs.rs/bytesbuf/0.5.5/bytesbuf/?search=BytesBuf
- [__link9]: https://docs.rs/multitude/0.3.1/multitude/?search=strings::String
+ [__link9]: https://docs.rs/multitude/0.3.2/multitude/?search=strings::String
diff --git a/crates/multitude/benches/criterion_arc_array.rs b/crates/multitude/benches/criterion_arc_array.rs
new file mode 100644
index 000000000..a24c4337b
--- /dev/null
+++ b/crates/multitude/benches/criterion_arc_array.rs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Builds an `Arc<[Arc<[u8]>]>` of `PROPERTIES` binary blobs two ways and
+//! compares them: `std::sync::Arc` (global allocator) vs `multitude::Arc`
+#![allow(clippy::unwrap_used, reason = "benchmark code")]
+#![allow(clippy::missing_panics_doc, reason = "benchmark code")]
+#![allow(unused_results, reason = "benchmark code")]
+#![allow(clippy::std_instead_of_core, reason = "benchmark code")]
+#![allow(dead_code, reason = "array properties are held only to keep the allocation alive")]
+
+use std::hint::black_box;
+use std::sync::Arc as StdArc;
+
+use criterion::{Criterion, criterion_group, criterion_main};
+use multitude::{Arc as ArenaArc, Arena};
+
+// ---------------------------------------------------------------------------
+// Array shape: `PROPERTIES` binary blobs of `PROPERTY_SIZE` bytes each.
+// ---------------------------------------------------------------------------
+
+const PROPERTIES: usize = 8;
+const PROPERTY_SIZE: usize = 16;
+
+// ---------------------------------------------------------------------------
+// Global-allocator array
+// ---------------------------------------------------------------------------
+
+fn build_global(payload: &[u8]) -> StdArc<[StdArc<[u8]>]> {
+ let mut properties = Vec::with_capacity(PROPERTIES);
+ for _ in 0..PROPERTIES {
+ properties.push(StdArc::<[u8]>::from(payload));
+ }
+ StdArc::from(properties)
+}
+
+fn build_global_from_slice(properties: &[StdArc<[u8]>]) -> StdArc<[StdArc<[u8]>]> {
+ StdArc::from(properties)
+}
+
+// ---------------------------------------------------------------------------
+// Arena-backed array
+// ---------------------------------------------------------------------------
+
+fn build_arena(arena: &Arena, payload: &[u8]) -> ArenaArc<[ArenaArc<[u8]>]> {
+ let mut properties = arena.alloc_vec_with_capacity::>(PROPERTIES);
+ for _ in 0..PROPERTIES {
+ properties.push(arena.alloc_slice_copy_arc(payload));
+ }
+ properties.try_into_arc().unwrap()
+}
+
+fn build_arena_from_slice(arena: &Arena, properties: &[StdArc<[u8]>]) -> ArenaArc<[StdArc<[u8]>]> {
+ arena.alloc_slice_clone_arc(properties)
+}
+
+fn global_properties(payload: &[u8]) -> Vec> {
+ (0..PROPERTIES).map(|_| StdArc::<[u8]>::from(payload)).collect()
+}
+
+// ---------------------------------------------------------------------------
+// Criterion timing + per-iteration allocation tracking
+// ---------------------------------------------------------------------------
+
+fn bench_arc_array(c: &mut Criterion) {
+ let payload = vec![0xABu8; PROPERTY_SIZE];
+
+ let mut group = c.benchmark_group("arc_array");
+
+ group.bench_function("global", |b| {
+ b.iter(|| {
+ black_box(build_global(black_box(&payload)));
+ });
+ });
+
+ let arena = Arena::new();
+ black_box(build_arena(&arena, &payload));
+
+ group.bench_function("arena", |b| {
+ b.iter(|| {
+ black_box(build_arena(&arena, black_box(&payload)));
+ });
+ });
+
+ let global_props = global_properties(&payload);
+ group.bench_function("global_from_slice", |b| {
+ b.iter(|| {
+ black_box(build_global_from_slice(black_box(&global_props)));
+ });
+ });
+
+ let work_arena = Arena::new();
+ black_box(build_arena_from_slice(&work_arena, &global_props));
+
+ group.bench_function("arena_from_slice", |b| {
+ b.iter(|| {
+ black_box(build_arena_from_slice(&work_arena, black_box(&global_props)));
+ });
+ });
+
+ group.finish();
+}
+
+criterion_group!(benches, bench_arc_array);
+criterion_main!(benches);
diff --git a/crates/multitude/benches/gungraun_arc_array/linux.rs b/crates/multitude/benches/gungraun_arc_array/linux.rs
new file mode 100644
index 000000000..39846f4ad
--- /dev/null
+++ b/crates/multitude/benches/gungraun_arc_array/linux.rs
@@ -0,0 +1,172 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Instruction-precise `Arc<[Arc<[u8]>]>` build benchmarks for multitude.
+//!
+//! Mirrors `benches/criterion_arc_array.rs` 1:1: each gungraun function
+//! `` corresponds to a criterion benchmark `arc_array/`.
+//! Builds an `Arc<[Arc<[u8]>]>` of `PROPERTIES` binary blobs two ways and
+//! compares them: `std::sync::Arc` (global allocator) vs `multitude::Arc`
+//! (arena). Each is built with two strategies:
+//!
+//! - `*` — push freshly allocated properties through a growable vec, then
+//! freeze it into the `Arc`.
+//! - `*_from_slice` — build directly from a pre-created slice of properties,
+//! with no intermediate vec.
+//!
+//! # Allocation hygiene
+//!
+//! Following the same toggle hygiene as `gungraun_alloc`: setup (the arena
+//! warm-up, the payload, the pre-created property slice, and the pre-sized
+//! output `Vec`) runs outside the callgrind toggle via `#[bench::run(...)]`.
+//! The timed body only builds the structures and pushes the handles into the
+//! pre-sized output `Vec`, which is returned by value so its `Drop` runs
+//! outside the toggle. The only traffic counted is the build itself.
+
+#![allow(missing_docs, reason = "Benchmark")]
+#![allow(unused_results, reason = "black_box of bench input is intentional")]
+#![allow(clippy::unwrap_used, reason = "benchmark code")]
+#![allow(
+ clippy::needless_pass_by_value,
+ reason = "gungraun bench inputs are passed by value by the framework"
+)]
+#![allow(clippy::type_complexity, reason = "benchmark state tuples are inherently complex")]
+#![allow(clippy::too_many_lines, reason = "benchmark file")]
+
+use core::hint::black_box;
+use std::sync::Arc as StdArc;
+
+use gungraun::{Callgrind, LibraryBenchmarkConfig, library_benchmark, library_benchmark_group, main};
+use multitude::{Arc as ArenaArc, Arena};
+
+// Array shape: `PROPERTIES` binary blobs of `PROPERTY_SIZE` bytes each, built
+// `N` times per bench so the per-build instruction count is stable.
+const PROPERTIES: usize = 8;
+const PROPERTY_SIZE: usize = 16;
+const N: usize = 1_000;
+
+type GlobalArray = StdArc<[StdArc<[u8]>]>;
+type ArenaArrayOfArena = ArenaArc<[ArenaArc<[u8]>]>;
+type ArenaArrayOfGlobal = ArenaArc<[StdArc<[u8]>]>;
+
+// ===== shared builders (mirror criterion_arc_array.rs) =====
+
+fn build_global(payload: &[u8]) -> GlobalArray {
+ let mut properties = Vec::with_capacity(PROPERTIES);
+ for _ in 0..PROPERTIES {
+ properties.push(StdArc::<[u8]>::from(payload));
+ }
+ StdArc::from(properties)
+}
+
+fn build_global_from_slice(properties: &[StdArc<[u8]>]) -> GlobalArray {
+ StdArc::from(properties)
+}
+
+fn build_arena(arena: &Arena, payload: &[u8]) -> ArenaArrayOfArena {
+ let mut properties = arena.alloc_vec_with_capacity::>(PROPERTIES);
+ for _ in 0..PROPERTIES {
+ properties.push(arena.alloc_slice_copy_arc(payload));
+ }
+ properties.try_into_arc().unwrap()
+}
+
+fn build_arena_from_slice(arena: &Arena, properties: &[StdArc<[u8]>]) -> ArenaArrayOfGlobal {
+ arena.alloc_slice_clone_arc(properties)
+}
+
+// ===== leaf setup helpers =====
+
+fn payload() -> Vec {
+ vec![0xAB_u8; PROPERTY_SIZE]
+}
+
+fn global_properties() -> Vec> {
+ let payload = payload();
+ (0..PROPERTIES).map(|_| StdArc::<[u8]>::from(payload.as_slice())).collect()
+}
+
+fn warm_arena() -> Arena {
+ // Warm: preallocate one chunk of the largest size class for each flavor
+ // AND prime the arena's current_local / current_shared mutators with a
+ // throwaway allocation, so the timed body never pays a cold `refill_*`.
+ // Mirrors `gungraun_alloc::warm_arena`.
+ let arena = Arena::builder()
+ .with_capacity_local(64 * 1024)
+ .with_capacity_shared(64 * 1024)
+ .build();
+ let _: &mut u64 = arena.alloc(0_u64);
+ let _ = arena.alloc_arc(0_u64);
+ arena
+}
+
+// ===== composite setups (pre-allocate the output Vec to N) =====
+
+fn setup_global() -> (Vec, Vec) {
+ (payload(), Vec::with_capacity(N))
+}
+
+fn setup_arena() -> (Arena, Vec, Vec) {
+ (warm_arena(), payload(), Vec::with_capacity(N))
+}
+
+fn setup_global_from_slice() -> (Vec>, Vec) {
+ (global_properties(), Vec::with_capacity(N))
+}
+
+fn setup_arena_from_slice() -> (Arena, Vec>, Vec) {
+ (warm_arena(), global_properties(), Vec::with_capacity(N))
+}
+
+// ===== bench bodies — only the build is inside the toggle =====
+
+#[library_benchmark]
+#[bench::run(setup_global())]
+fn global(state: (Vec, Vec)) -> (Vec, Vec) {
+ let (payload, mut out) = state;
+ for _ in 0..N {
+ out.push(black_box(build_global(black_box(&payload))));
+ }
+ (payload, out)
+}
+
+#[library_benchmark]
+#[bench::run(setup_arena())]
+fn arena(state: (Arena, Vec, Vec)) -> (Arena, Vec, Vec) {
+ let (arena, payload, mut out) = state;
+ for _ in 0..N {
+ out.push(black_box(build_arena(&arena, black_box(&payload))));
+ }
+ (arena, payload, out)
+}
+
+#[library_benchmark]
+#[bench::run(setup_global_from_slice())]
+fn global_from_slice(state: (Vec>, Vec)) -> (Vec>, Vec) {
+ let (properties, mut out) = state;
+ for _ in 0..N {
+ out.push(black_box(build_global_from_slice(black_box(&properties))));
+ }
+ (properties, out)
+}
+
+#[library_benchmark]
+#[bench::run(setup_arena_from_slice())]
+fn arena_from_slice(state: (Arena, Vec>, Vec)) -> (Arena, Vec>, Vec) {
+ let (arena, properties, mut out) = state;
+ for _ in 0..N {
+ out.push(black_box(build_arena_from_slice(&arena, black_box(&properties))));
+ }
+ (arena, properties, out)
+}
+
+library_benchmark_group!(
+ name = arc_array_group;
+ benchmarks = global, arena, global_from_slice, arena_from_slice
+);
+
+main!(
+ config = LibraryBenchmarkConfig::default()
+ .tool(Callgrind::with_args(["--branch-sim=yes"]));
+ library_benchmark_groups = arc_array_group
+);
diff --git a/crates/multitude/benches/gungraun_arc_array/main.rs b/crates/multitude/benches/gungraun_arc_array/main.rs
new file mode 100644
index 000000000..f28534120
--- /dev/null
+++ b/crates/multitude/benches/gungraun_arc_array/main.rs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Instruction-precise `Arc<[Arc<[u8]>]>` build benchmarks for multitude.
+//!
+//! Mirrors `benches/criterion_arc_array.rs` 1:1: each gungraun function
+//! `` corresponds to a criterion benchmark `arc_array/`.
+//!
+//! Run with `cargo bench --bench gungraun_arc_array` on a Linux host with
+//! Valgrind.
+
+#![allow(missing_docs, reason = "Benchmark")]
+#![allow(unused_results, reason = "black_box of bench input is intentional")]
+#![allow(
+ clippy::needless_pass_by_value,
+ reason = "gungraun bench inputs are passed by value by the framework"
+)]
+#![allow(clippy::type_complexity, reason = "benchmark state tuples are inherently complex")]
+#![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."
+ )
+)]
+
+// 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 = arc_array_group
+);
diff --git a/crates/multitude/docs/BUMPALO.md b/crates/multitude/docs/BUMPALO.md
index 1229dde9e..287413c36 100644
--- a/crates/multitude/docs/BUMPALO.md
+++ b/crates/multitude/docs/BUMPALO.md
@@ -12,7 +12,7 @@ spirit; here's how multitude differs.
| Refcounted smart pointers | ❌ (raw `&'bump T`) | ✅ `Arc` (atomic; thread-safe sharing) |
| Smart pointers outlive the arena | ❌ | ✅ (`Arc` / `Box` and their `str` variants — simple references are lifetime-bound) |
| Cross-thread sharing of individual values | ❌ | ✅ via `Arc` |
-| Automatic per-object `Drop` | Only via `bumpalo::boxed::Box` | ✅ Automatic (refcount smart pointers drop at chunk teardown; `Box` drops at smart pointer drop; simple references drop at arena drop) |
+| Automatic per-object `Drop` | Only via `bumpalo::boxed::Box` | ✅ Automatic (`Arc` drops at last-clone drop, `Box` drops at smart-pointer drop, simple references drop at arena reset/drop) |
| Owned single smart pointer (`Drop` on drop) | `bumpalo::boxed::Box` | `Box` |
| Smart-pointer width | 16 bytes for fat DSTs (`&str`, `Bump-allocated boxed slice` are 2-word) | **8 bytes uniformly** — `Arc` / `Box` are thin even for DST `T` (slice / `str` / `dyn Trait` / custom `Pointee`); DST metadata is stored unaligned in a chunk prefix |
| Single-pointer string smart pointers | ❌ (`&str` is 16 bytes) | ✅ `Arc` / `Box` / `ArcUtf16Str` / `BoxUtf16Str` are all 8 bytes (length stored unaligned in a `usize` prefix in the chunk; zero per-string padding) |
diff --git a/crates/multitude/docs/DESIGN.md b/crates/multitude/docs/DESIGN.md
index bdb688334..f58983365 100644
--- a/crates/multitude/docs/DESIGN.md
+++ b/crates/multitude/docs/DESIGN.md
@@ -72,13 +72,17 @@ the shared chunk's `AtomicUsize` refcount on every allocation would be a
hot-path atomic. Instead, at install time the arena pre-credits the
chunk's atomic `ref_count` with `LARGE_SHARED_REF_SURPLUS` (2^30) and
tracks per-allocation handouts in the non-atomic `local_shared_count`
-(`Cell`). At retire (refill / reset / arena drop) the surplus is
-reconciled with a single
+(`Cell`). At retire (`refill_shared` or `Arena::drop`) the surplus
+is reconciled with a single
`fetch_sub(LARGE_SHARED_REF_SURPLUS - local_shared_count)`, leaving the
chunk's atomic count equal to the number of escaped handles. The 2^30
surplus is large enough that concurrent `Arc::drop` on other threads
-cannot underflow it, while the `u32` counter leaves ~2^30 headroom
-against `Arc::clone` overflow.
+cannot underflow it. `Arc::clone` does not touch this count —
+each `Arc` family takes exactly one chunk refcount at allocation and
+releases it when its last clone drops (clones bump only the per-`Arc`
+strong count; see *Per-`Arc` reference counting*). `Arena::reset` does
+not reconcile or detach the installed shared chunk — it resets only
+local-chunk state, so shared allocations continue on the same chunk.
**Size-class ratchet.** Each successful refill bumps the matching
`next_*_class` toward the largest cacheable class (`NUM_CHUNK_CLASSES
@@ -155,7 +159,6 @@ pub(crate) struct SharedChunk {
capacity: usize,
ref_count: AtomicUsize,
next: AtomicPtr, // intrusive cache-freelist link
- drop_entry_count: AtomicU16,
#[cfg(feature = "stats")]
wasted_at_retire: AtomicU32,
data: [UnsafeCell],
@@ -209,7 +212,10 @@ is a **single 8-byte raw pointer** into the chunk's `data` tail. DST
metadata (slice length, vtable) lives unaligned in the chunk prefix
immediately preceding the value payload, read with
`core::ptr::read_unaligned`. For `T: Sized` the metadata is `()` so
-there's no prefix overhead.
+there's no prefix overhead. `Arc` additionally stores its
+per-`Arc` strong count (an `AtomicU32`) in the prefix, before the
+metadata (see *Per-`Arc` reference counting*); `Box` has no such
+prefix.
To recover the owning chunk's header from a smart-pointer value, each
smart-pointer type **masks the low bits to the 64 KiB boundary**
@@ -237,23 +243,64 @@ Two consequences of the masking scheme:
refill path if a ZST would otherwise land at the one-past-end
boundary.
+## Per-`Arc` reference counting
+
+Each `Arc` carries **its own** strong reference count — an
+`AtomicU32` stored in the chunk payload immediately *before* the value
+(and before the DST metadata, if any). The layout of an `Arc` value is:
+
+```text
+[strong (AtomicU32, at reservation base)][pad][T::Metadata (unaligned)][T payload]
+ ^ value pointer
+```
+
+The reservation is aligned to `max(align_of::(), 4)` so the leading
+strong slot is 4-byte aligned; the value pointer is `align_of::()`
+aligned and the metadata sits immediately before it (recovered with
+`read_unaligned`, exactly as for `Box`). The strong count is recovered
+from the value pointer by subtracting a fixed prefix
+(`thin_dst::strong_prefix_bytes_for`) and is accessed only as an
+`AtomicU32` — never through a reference that spans the (possibly
+uninitialized) payload, which keeps the scheme sound under Miri.
+
+The accounting works as follows:
+
+- **Allocation** writes `strong = 1` and takes **one** refcount on the
+ hosting chunk for the whole `Arc` family (via the pre-credited
+ surplus, as for any shared allocation).
+- **`Arc::clone`** bumps only the per-`Arc` `strong` with a single
+ `Relaxed` increment — it does **not** touch the chunk refcount.
+- **`Arc::drop`** does a `Release` decrement of `strong`; on the
+ `strong → 0` transition it runs an `Acquire` fence, drops the value
+ in place (`drop_in_place::`, which natively handles `?Sized`),
+ and releases the family's single chunk refcount (adopted *before*
+ the value drop, so a panicking destructor still releases the chunk).
+
+Because the value's destructor runs eagerly on the last `Arc` (rather
+than being deferred to chunk teardown), nested arena `Arc`s — e.g.
+`Arc<[Arc]>` whose inner and outer handles share a chunk — release
+their storage promptly instead of forming a self-pinning cycle.
+
+`Arc::>::assume_init` is a pure reinterpret: `MaybeUninit`
+and `T` share size, alignment, and metadata, so the strong-prefix layout
+is identical and the strong count is untouched.
+
## `DropEntry`
-`DropEntry` records the deferred destructor work for values whose
-`Drop` cannot be run by the smart pointer itself — i.e. arena
-references (`&mut T` / `&mut [T]`, which have no `Drop` of their own)
-and `Arc` (whose value must be dropped by whichever handle observes
-the last refcount, a moment only the chunk can detect). **No `Box`
-variant registers a drop entry**: `Box::drop` runs `drop_in_place` on
-the (re-fattened) value pointer eagerly, which natively handles `?Sized`
-`T`, so sized `Box`, slice `Box<[T]>`, and DST `Box` all
-need no entry.
-
-Each such allocation reserves **both** `size_of::()` at the front
-of the free region *and* one `DropEntry` slot at the back. The
-effective remaining capacity is `drop_top - bump`; overflow is
-detected when those two meet. Allocations of `T: !Drop` skip the
-reservation entirely.
+`DropEntry` records the deferred destructor work for **local arena
+references only** — `Arena::alloc -> &mut T` and `&mut [T]`, which have
+no `Drop` of their own and whose backing chunk runs the destructor at
+teardown. **Neither `Box` nor `Arc` registers a drop entry, and shared
+chunks never carry one**: `Box::drop` runs `drop_in_place` eagerly on
+the (re-fattened) value pointer, and `Arc::drop` does the same on the
+last strong reference (see *Per-`Arc` reference counting* above). Drop
+entries therefore live exclusively on `LocalChunk`s.
+
+Each such reference allocation reserves **both** `size_of::()` at the
+front of the free region *and* one `DropEntry` slot at the back. The
+effective remaining capacity is `drop_top - bump`; overflow is detected
+when those two meet. Allocations of `T: !Drop` skip the reservation
+entirely.
```rust
#[repr(C)]
@@ -266,9 +313,11 @@ struct DropEntry {
}
```
-`len` is a `u16`; slice/DST allocations whose `needs_drop` count
+`len` is a `u16`; local slice references whose `needs_drop` count
exceeds `u16::MAX` are rejected up front by their `alloc_*` orchestrator
-so the placeholder never overflows.
+so the placeholder never overflows. (The `Arc<[T]>` family has **no**
+such cap, since it drops via `drop_in_place::<[T]>` rather than a
+counted entry.)
**Two-phase write.** Allocation paths reserve a *placeholder* (null
`drop_fn`, real `value_offset`/`len`) up front. After the value is
@@ -279,26 +328,27 @@ initialization closure panicked or whose `Uninit` ticket was dropped
without `init`. Storing as `AtomicPtr<()>` (not `AtomicUsize`)
preserves function-pointer provenance under Miri's strict provenance.
-The commit is idempotent: concurrent `Arc::>::assume_init`
-on cloned handles all install the same `T`-determined shim.
-
-**Replay.** When the chunk's last refcount drops, the chunk walks its
-drop-entry stack **newest-first** (LIFO, matching Rust drop order) and
-invokes `(drop_fn)(data + value_offset, len)` on each committed
-entry. A panic in any shim is contained; replay continues so remaining
-destructors still run.
+**Replay.** When a `LocalChunk`'s refcount drops to zero (at
+`Arena::reset` / `Arena::drop`), the chunk walks its drop-entry stack
+**newest-first** (LIFO, matching Rust drop order) and invokes
+`(drop_fn)(data + value_offset, len)` on each committed entry. Shared
+chunks skip this step entirely. A panic in any shim is contained;
+replay continues so remaining destructors still run.
**Closure-panic safety.** The smart-pointer construction paths take a
protective `ChunkRef` (`+1` guard) before invoking the user closure.
On unwinding, the `ChunkRef`'s `Drop` releases the +1; on success the
caller calls `ChunkRef::forget` to transfer the +1 into the
freshly-constructed smart pointer. Combined with the two-phase
-placeholder, a panicking closure leaves no `T::drop` queued on
+placeholder (for local references) and eager `drop_in_place` (for
+`Box`/`Arc`), a panicking closure leaves no `T::drop` queued on
uninitialized memory and no refcount leaked.
-**Refcount overflow.** Both `inc_ref` paths check against the
-wraparound boundary and abort (`std::process::abort` or a forced
-double-panic under `no_std`) if exceeded. The abort helper is
-`#[cold] #[inline(never)]` so the hot-path call site stays small.
-This mirrors `std::sync::Arc`: a wraparound would race live pointers
-with a free, and the only sound response is to terminate.
+**Refcount overflow.** Both the chunk `inc_ref` paths and `Arc::clone`'s
+per-`Arc` `strong` increment check against the wraparound boundary and
+abort (`std::process::abort` or a forced double-panic under `no_std`) if
+exceeded. The abort helper is `#[cold] #[inline(never)]` so the hot-path
+call site stays small. This mirrors `std::sync::Arc`: a wraparound would
+race live pointers with a free, and the only sound response is to
+terminate.
+
diff --git a/crates/multitude/docs/PERF.md b/crates/multitude/docs/PERF.md
index 173e127e6..e41eb71d5 100644
--- a/crates/multitude/docs/PERF.md
+++ b/crates/multitude/docs/PERF.md
@@ -13,98 +13,98 @@ Bench names are aligned between criterion and gungraun via the `GROUPS` table in
| Variant | Time (criterion) | Instructions | Branch misses | Mem accesses |
|---|---:|---:|---:|---:|
-| `multitude_new` | 38 ns | 316 | 9 | 457 |
+| `multitude_new` | 37 ns | 316 | 8 | 457 |
| `bumpalo_new` | 1 ns | 16 | 1 | 26 |
## `alloc_u64`
| Variant | Time (criterion) | Instructions | Branch misses | Mem accesses |
|---|---:|---:|---:|---:|
-| `alloc` | 6.45 µs | 14,026 | 6 | 21,043 |
-| `alloc_with` | 6.53 µs | 14,024 | 11 | 21,040 |
-| `alloc_box` | 5.24 µs | 23,043 | 9 | 37,078 |
-| `alloc_box_with` | 5.18 µs | 24,043 | 9 | 38,078 |
-| `alloc_uninit_box` | 2.33 µs | 20,043 | 9 | 31,078 |
-| `alloc_zeroed_box` | 4.85 µs | 21,043 | 9 | 33,078 |
-| `alloc_arc` | 5.36 µs | 23,043 | 7 | 37,078 |
-| `alloc_arc_with` | 5.19 µs | 24,043 | 9 | 38,078 |
-| `alloc_uninit_arc` | 2.33 µs | 20,043 | 9 | 31,078 |
-| `alloc_zeroed_arc` | 4.96 µs | 21,043 | 9 | 33,078 |
-| `bumpalo_alloc` | 5.97 µs | 19,022 | 4 | 27,037 |
-| `bumpalo_alloc_with` | 6.08 µs | 19,020 | 4 | 27,034 |
+| `alloc` | 6.58 µs | 14,026 | 6 | 21,043 |
+| `alloc_with` | 6.62 µs | 14,024 | 9 | 21,040 |
+| `alloc_box` | 5.83 µs | 23,043 | 9 | 37,078 |
+| `alloc_box_with` | 5.94 µs | 24,043 | 9 | 38,078 |
+| `alloc_uninit_box` | 3.10 µs | 20,043 | 9 | 31,078 |
+| `alloc_zeroed_box` | 5.58 µs | 21,043 | 9 | 33,078 |
+| `alloc_arc` | 9.48 µs | 25,043 | 9 | 40,078 |
+| `alloc_arc_with` | 9.78 µs | 26,043 | 8 | 41,078 |
+| `alloc_uninit_arc` | 9.26 µs | 22,043 | 9 | 34,078 |
+| `alloc_zeroed_arc` | 9.53 µs | 23,043 | 9 | 36,078 |
+| `bumpalo_alloc` | 6.57 µs | 19,022 | 6 | 27,037 |
+| `bumpalo_alloc_with` | 6.58 µs | 19,020 | 4 | 27,034 |
## `alloc_str`
| Variant | Time (criterion) | Instructions | Branch misses | Mem accesses |
|---|---:|---:|---:|---:|
-| `alloc_str` | 8.24 µs | 51,053 | 10 | 76,098 |
-| `alloc_str_box` | 11.83 µs | 59,053 | 11 | 85,098 |
-| `alloc_str_arc` | 11.89 µs | 59,053 | 11 | 85,098 |
-| `bumpalo_alloc_str` | 9.13 µs | 50,048 | 13 | 75,088 |
+| `alloc_str` | 8.36 µs | 51,053 | 10 | 76,098 |
+| `alloc_str_box` | 12.64 µs | 59,053 | 11 | 85,098 |
+| `alloc_str_arc` | 14.00 µs | 58,054 | 11 | 84,099 |
+| `bumpalo_alloc_str` | 9.56 µs | 50,048 | 13 | 75,088 |
## `alloc_slice`
| Variant | Time (criterion) | Instructions | Branch misses | Mem accesses |
|---|---:|---:|---:|---:|
-| `alloc_slice_copy` | 22.82 µs | 41,049 | 4 | 57,090 |
-| `alloc_slice_clone` | 22.50 µs | 45,050 | 10 | 58,091 |
-| `alloc_slice_fill_with` | 24.07 µs | 38,026 | 11 | 68,043 |
-| `alloc_slice_fill_iter` | 24.18 µs | 38,027 | 11 | 68,044 |
-| `alloc_slice_copy_box` | 41.99 µs | 55,646 | 33 | 83,916 |
-| `alloc_slice_clone_box` | 42.18 µs | 68,646 | 40 | 92,915 |
-| `alloc_slice_fill_with_box` | 43.59 µs | 48,585 | 40 | 86,809 |
-| `alloc_slice_fill_iter_box` | 43.84 µs | 50,585 | 39 | 90,809 |
-| `alloc_uninit_slice_box` | 39.83 µs | 23,585 | 40 | 36,809 |
-| `alloc_zeroed_slice_box` | 40.73 µs | 27,585 | 40 | 43,809 |
-| `alloc_slice_copy_arc` | 42.49 µs | 53,647 | 34 | 80,917 |
-| `alloc_slice_clone_arc` | 42.37 µs | 59,645 | 39 | 80,914 |
-| `alloc_slice_fill_with_arc` | 44.47 µs | 46,585 | 41 | 82,809 |
-| `alloc_slice_fill_iter_arc` | 43.77 µs | 47,585 | 40 | 84,809 |
-| `alloc_uninit_slice_arc` | 40.01 µs | 22,585 | 40 | 34,809 |
-| `alloc_zeroed_slice_arc` | 41.27 µs | 25,585 | 40 | 39,809 |
-| `bumpalo_alloc_slice_copy` | 23.49 µs | 38,042 | 4 | 55,076 |
-| `bumpalo_alloc_slice_clone` | 24.38 µs | 60,046 | 9 | 74,083 |
-| `bumpalo_alloc_slice_fill_with` | 25.44 µs | 40,020 | 5 | 70,033 |
-| `bumpalo_alloc_slice_fill_iter` | 25.43 µs | 40,020 | 5 | 70,033 |
+| `alloc_slice_copy` | 33.81 µs | 41,049 | 3 | 57,090 |
+| `alloc_slice_clone` | 33.49 µs | 45,050 | 10 | 58,091 |
+| `alloc_slice_fill_with` | 35.52 µs | 38,026 | 10 | 68,043 |
+| `alloc_slice_fill_iter` | 35.82 µs | 38,027 | 9 | 68,044 |
+| `alloc_slice_copy_box` | 50.31 µs | 55,624 | 28 | 83,885 |
+| `alloc_slice_clone_box` | 49.03 µs | 68,624 | 36 | 92,884 |
+| `alloc_slice_fill_with_box` | 51.45 µs | 48,563 | 31 | 86,778 |
+| `alloc_slice_fill_iter_box` | 52.38 µs | 50,563 | 34 | 90,778 |
+| `alloc_uninit_slice_box` | 46.89 µs | 23,563 | 34 | 36,778 |
+| `alloc_zeroed_slice_box` | 48.11 µs | 27,563 | 34 | 43,778 |
+| `alloc_slice_copy_arc` | 54.53 µs | 55,625 | 28 | 83,886 |
+| `alloc_slice_clone_arc` | 54.06 µs | 61,623 | 36 | 83,883 |
+| `alloc_slice_fill_with_arc` | 56.68 µs | 47,563 | 33 | 84,778 |
+| `alloc_slice_fill_iter_arc` | 55.84 µs | 48,563 | 32 | 86,778 |
+| `alloc_uninit_slice_arc` | 51.35 µs | 23,563 | 34 | 36,778 |
+| `alloc_zeroed_slice_arc` | 51.95 µs | 26,563 | 33 | 41,778 |
+| `bumpalo_alloc_slice_copy` | 36.94 µs | 38,042 | 7 | 55,076 |
+| `bumpalo_alloc_slice_clone` | 36.81 µs | 60,046 | 10 | 74,083 |
+| `bumpalo_alloc_slice_fill_with` | 36.03 µs | 40,020 | 5 | 70,033 |
+| `bumpalo_alloc_slice_fill_iter` | 37.52 µs | 40,020 | 5 | 70,033 |
## `string_builder`
| Variant | Time (criterion) | Instructions | Branch misses | Mem accesses |
|---|---:|---:|---:|---:|
-| `alloc_string` | 8.05 µs | 36,836 | 32 | 51,184 |
-| `alloc_string_with_capacity` | 7.64 µs | 37,194 | 21 | 52,304 |
-| `bumpalo_string_new_in` | 9.20 µs | 35,843 | 76 | 50,867 |
-| `bumpalo_string_with_capacity_in` | 10.62 µs | 34,708 | 28 | 49,159 |
+| `alloc_string` | 8.23 µs | 36,849 | 28 | 51,203 |
+| `alloc_string_with_capacity` | 8.09 µs | 37,210 | 20 | 52,325 |
+| `bumpalo_string_new_in` | 12.16 µs | 35,843 | 74 | 50,867 |
+| `bumpalo_string_with_capacity_in` | 11.79 µs | 34,708 | 30 | 49,159 |
## `vec_builder`
| Variant | Time (criterion) | Instructions | Branch misses | Mem accesses |
|---|---:|---:|---:|---:|
-| `alloc_vec` | 1.25 µs | 11,765 | 31 | 17,053 |
-| `alloc_vec_with_capacity` | 1.23 µs | 12,132 | 8 | 18,215 |
-| `bumpalo_vec_new_in` | 3.72 µs | 12,281 | 61 | 18,888 |
-| `bumpalo_vec_with_capacity_in` | 3.48 µs | 11,069 | 2 | 17,116 |
+| `alloc_vec` | 1.29 µs | 11,792 | 30 | 17,087 |
+| `alloc_vec_with_capacity` | 1.23 µs | 12,139 | 10 | 18,221 |
+| `bumpalo_vec_new_in` | 3.89 µs | 12,281 | 61 | 18,888 |
+| `bumpalo_vec_with_capacity_in` | 3.63 µs | 11,069 | 2 | 17,116 |
## `drop`
| Variant | Time (criterion) | Instructions | Branch misses | Mem accesses |
|---|---:|---:|---:|---:|
-| `box_u64` | 8.42 µs | 10,309 | 55 | 13,904 |
-| `rc_u64` | 8.18 µs | 10,309 | 55 | 13,904 |
-| `arc_u64` | 8.36 µs | 10,309 | 55 | 13,904 |
-| `box_droppy` | 22.06 µs | 186,161 | 77 | 272,621 |
-| `rc_droppy` | 27.37 µs | 219,386 | 80 | 320,930 |
-| `arc_droppy` | 27.25 µs | 219,386 | 80 | 320,930 |
-| `str_box` | 7.59 µs | 10,309 | 55 | 13,904 |
-| `str_rc` | 7.68 µs | 10,309 | 55 | 13,904 |
-| `str_arc` | 7.71 µs | 10,309 | 55 | 13,904 |
-| `slice_box_u64` | 13.96 µs | 10,819 | 58 | 14,639 |
-| `slice_rc_u64` | 12.30 µs | 10,819 | 58 | 14,639 |
-| `slice_arc_u64` | 12.59 µs | 10,819 | 58 | 14,639 |
-| `slice_box_droppy` | 115.72 µs | 1,520,210 | 1,848 | 2,214,775 |
-| `slice_rc_droppy` | 122.93 µs | 1,546,283 | 1,110 | 2,253,860 |
-| `slice_arc_droppy` | 122.05 µs | 1,546,283 | 1,110 | 2,253,860 |
-| `alloc` | 686 ns | 337 | 15 | 504 |
+| `box_u64` | 8.05 µs | 10,660 | 68 | 14,433 |
+| `rc_u64` | 12.85 µs | 13,005 | 64 | 18,929 |
+| `arc_u64` | 12.53 µs | 13,005 | 64 | 18,929 |
+| `box_droppy` | 15.19 µs | 186,501 | 86 | 273,127 |
+| `rc_droppy` | 15.53 µs | 188,852 | 83 | 277,632 |
+| `arc_droppy` | 20.20 µs | 188,852 | 83 | 277,632 |
+| `str_box` | 7.50 µs | 10,660 | 68 | 14,433 |
+| `str_rc` | 12.30 µs | 13,005 | 70 | 18,929 |
+| `str_arc` | 12.13 µs | 13,005 | 70 | 18,929 |
+| `slice_box_u64` | 14.68 µs | 11,395 | 68 | 15,498 |
+| `slice_rc_u64` | 19.22 µs | 13,390 | 63 | 19,490 |
+| `slice_arc_u64` | 19.27 µs | 13,390 | 63 | 19,490 |
+| `slice_box_droppy` | 123.93 µs | 1,480,204 | 1,362 | 2,162,703 |
+| `slice_rc_droppy` | 122.57 µs | 1,482,204 | 1,107 | 2,166,702 |
+| `slice_arc_droppy` | 123.82 µs | 1,482,204 | 1,107 | 2,166,702 |
+| `alloc` | 970 ns | 345 | 13 | 514 |
## Multitude vs Bumpalo Head-to-Head
@@ -112,14 +112,14 @@ Direct comparisons of multitude versus bumpalo on identical workloads (the multi
| Workload | Multitude time | Bumpalo time | Δ time | Multitude instr | Bumpalo instr | Δ instr |
|---|---:|---:|---:|---:|---:|---:|
-| `alloc` vs `bumpalo_alloc` | 6.45 µs | 5.97 µs | +8.1% | 14,026 | 19,022 | -26.3% |
-| `alloc_str` vs `bumpalo_alloc_str` | 8.24 µs | 9.13 µs | -9.7% | 51,053 | 50,048 | +2.0% |
-| `alloc_slice_copy` vs `bumpalo_alloc_slice_copy` | 22.82 µs | 23.49 µs | -2.9% | 41,049 | 38,042 | +7.9% |
-| `alloc_slice_clone` vs `bumpalo_alloc_slice_clone` | 22.50 µs | 24.38 µs | -7.7% | 45,050 | 60,046 | -25.0% |
-| `alloc_slice_fill_with` vs `bumpalo_alloc_slice_fill_with` | 24.07 µs | 25.44 µs | -5.4% | 38,026 | 40,020 | -5.0% |
-| `alloc_slice_fill_iter` vs `bumpalo_alloc_slice_fill_iter` | 24.18 µs | 25.43 µs | -4.9% | 38,027 | 40,020 | -5.0% |
-| `alloc_string` vs `bumpalo_string_new_in` | 8.05 µs | 9.20 µs | -12.5% | 36,836 | 35,843 | +2.8% |
-| `alloc_string_with_capacity` vs `bumpalo_string_with_capacity_in` | 7.64 µs | 10.62 µs | -28.0% | 37,194 | 34,708 | +7.2% |
-| `alloc_vec` vs `bumpalo_vec_new_in` | 1.25 µs | 3.72 µs | -66.3% | 11,765 | 12,281 | -4.2% |
-| `alloc_vec_with_capacity` vs `bumpalo_vec_with_capacity_in` | 1.23 µs | 3.48 µs | -64.6% | 12,132 | 11,069 | +9.6% |
+| `alloc` vs `bumpalo_alloc` | 6.58 µs | 6.57 µs | +0.2% | 14,026 | 19,022 | -26.3% |
+| `alloc_str` vs `bumpalo_alloc_str` | 8.36 µs | 9.56 µs | -12.5% | 51,053 | 50,048 | +2.0% |
+| `alloc_slice_copy` vs `bumpalo_alloc_slice_copy` | 33.81 µs | 36.94 µs | -8.5% | 41,049 | 38,042 | +7.9% |
+| `alloc_slice_clone` vs `bumpalo_alloc_slice_clone` | 33.49 µs | 36.81 µs | -9.0% | 45,050 | 60,046 | -25.0% |
+| `alloc_slice_fill_with` vs `bumpalo_alloc_slice_fill_with` | 35.52 µs | 36.03 µs | -1.4% | 38,026 | 40,020 | -5.0% |
+| `alloc_slice_fill_iter` vs `bumpalo_alloc_slice_fill_iter` | 35.82 µs | 37.52 µs | -4.5% | 38,027 | 40,020 | -5.0% |
+| `alloc_string` vs `bumpalo_string_new_in` | 8.23 µs | 12.16 µs | -32.3% | 36,849 | 35,843 | +2.8% |
+| `alloc_string_with_capacity` vs `bumpalo_string_with_capacity_in` | 8.09 µs | 11.79 µs | -31.3% | 37,210 | 34,708 | +7.2% |
+| `alloc_vec` vs `bumpalo_vec_new_in` | 1.29 µs | 3.89 µs | -66.8% | 11,792 | 12,281 | -4.0% |
+| `alloc_vec_with_capacity` vs `bumpalo_vec_with_capacity_in` | 1.23 µs | 3.63 µs | -66.1% | 12,139 | 11,069 | +9.7% |
diff --git a/crates/multitude/src/allocator_impl.rs b/crates/multitude/src/allocator_impl.rs
index 4c7f2c67c..daef0e31e 100644
--- a/crates/multitude/src/allocator_impl.rs
+++ b/crates/multitude/src/allocator_impl.rs
@@ -56,7 +56,7 @@ unsafe impl Allocator for &Arena {
let _ = chunk_ref.forget();
return Ok(NonNull::slice_from_raw_parts(ptr, layout.size()));
}
- if self.is_oversized_shared(refill_hint) {
+ if self.is_oversized(refill_hint) {
return self.alloc_oversized_shared_with(refill_hint, |mutator, chunk_ptr| {
let (slot, _chunk) = mutator
.try_alloc_with_chunk(layout.size(), layout.align())
diff --git a/crates/multitude/src/arc.rs b/crates/multitude/src/arc.rs
index 37a51e2cb..afed3b0b9 100644
--- a/crates/multitude/src/arc.rs
+++ b/crates/multitude/src/arc.rs
@@ -10,35 +10,43 @@ use core::marker::PhantomData;
use core::mem::{self, MaybeUninit};
use core::pin::Pin;
use core::ptr::{self, NonNull};
+use core::sync::atomic::{Ordering, fence};
use allocator_api2::alloc::{Allocator, Global};
use ptr_meta::Pointee;
-use crate::internal::chunk::Chunk;
use crate::internal::chunk_ref::ChunkRef;
-use crate::internal::drop_entry::{self, DropFn};
-use crate::internal::shared_chunk::SharedChunk;
+use crate::internal::constants::refcount_overflow_abort;
use crate::internal::thin_dst;
use crate::thin_smart_ptr_common::impl_thin_smart_ptr_common;
use crate::vec::Vec;
+/// Strong-count saturation threshold. Cloning past this aborts the
+/// process, mirroring `std::sync::Arc`'s `MAX_REFCOUNT` guard (using
+/// the `u32` strong counter's half-range instead of `isize::MAX`).
+const MAX_STRONG_REFCOUNT: u32 = u32::MAX >> 1;
+
/// A thread-safe reference-counted smart pointer to a `T` stored in an [`Arena`](crate::Arena).
///
/// Safe to share across threads when `T: Send + Sync`.
///
/// Created via [`Arena::alloc_arc`](crate::Arena::alloc_arc). Cloning is
-/// **O(1)** and uses a single Relaxed atomic increment (matching
-/// `std::sync::Arc`). Dropping a clone is one Release decrement plus,
-/// on the final dec to zero, an Acquire fence before chunk teardown.
+/// **O(1)** and uses a single Relaxed atomic increment of the `Arc`'s
+/// own strong count (matching `std::sync::Arc`). Dropping a clone is one
+/// Release decrement plus, on the final dec to zero, an Acquire fence,
+/// the value's destructor (`T::drop`), and the release of the chunk
+/// reference.
///
-/// `Arc` keeps its containing chunk alive by holding a +1 refcount on
-/// it, so the smart pointer can outlive the arena it came from and
-/// survives [`Arena::reset`](crate::Arena::reset). For `T: Drop`, a
-/// drop entry is registered at allocation time and `T::drop` runs at
-/// chunk teardown (when the chunk's last reference is released); for
-/// `T: !Drop` (the common case for strings, numbers, slices, etc.),
-/// no drop entry is reserved and the only per-allocation cost beyond
-/// the value itself is the chunk's atomic refcount.
+/// Each `Arc` carries its own strong reference count — an
+/// [`AtomicU32`](core::sync::atomic::AtomicU32) stored in the chunk's
+/// payload immediately before the value. The allocation also holds
+/// **one** refcount on its containing chunk for the whole `Arc` family
+/// (all clones share it); that chunk reference is released only when the
+/// last `Arc` drops. This keeps the value alive across
+/// [`Arena::reset`](crate::Arena::reset) and lets the `Arc` outlive the
+/// arena, while running `T::drop` eagerly on the last drop — so nested
+/// `Arc`s (e.g. `Arc<[Arc]>`) release their storage promptly instead
+/// of deferring to chunk teardown.
///
/// # Pinning
///
@@ -86,13 +94,17 @@ impl Arc {
///
/// - `thin` must reference the payload of a fully-initialized `T`
/// whose storage was bump-allocated from a [`SharedChunk`] via
- /// the thin-DST allocator path. For DST `T` the chunk prefix
- /// must carry the matching `T::Metadata`. For `T: Drop`, a drop
- /// entry must already be registered so the destructor runs at
- /// chunk teardown.
+ /// the strong-prefixed `Arc` allocator path: a per-`Arc`
+ /// [`AtomicU32`](core::sync::atomic::AtomicU32) strong count must
+ /// already be initialized in the chunk prefix (see
+ /// [`thin_dst::strong_ref`](crate::internal::thin_dst::strong_ref)),
+ /// and for DST `T` the prefix must also carry the matching
+ /// `T::Metadata`.
/// - The caller must have just acquired a +1 refcount on that chunk
- /// in the new `Arc`'s name; the returned `Arc` takes ownership of
- /// that +1 and releases it in [`Drop`].
+ /// for the new `Arc` family, and the strong count must account for
+ /// this handle; the returned `Arc` owns that strong reference and
+ /// releases the chunk +1 (plus runs `T::drop`) when the strong
+ /// count reaches zero.
/// - `thin` must lie within the first `CHUNK_ALIGN` bytes of the
/// chunk so the header-from-mask helper recovers the chunk
/// address correctly.
@@ -134,33 +146,17 @@ impl Arc, A> {
/// The `MaybeUninit` must contain a fully-initialized, valid
/// `T`. The allocation must come from
/// [`Arena::alloc_uninit_arc`](crate::Arena::alloc_uninit_arc) or
- /// [`Arena::alloc_zeroed_arc`](crate::Arena::alloc_zeroed_arc) so a
- /// drop entry was reserved up front;
- /// `Arena::alloc_arc(MaybeUninit::new(...))` does not reserve one
- /// and panics here for `T: Drop`.
- ///
- /// # Panics
- ///
- /// Panics for `T: Drop` when no drop entry is found in the chunk
- /// — see the safety contract above.
+ /// [`Arena::alloc_zeroed_arc`](crate::Arena::alloc_zeroed_arc).
#[inline]
#[must_use]
pub unsafe fn assume_init(self) -> Arc {
- if const { mem::needs_drop::() } {
- // SAFETY: `self.ptr` references a live value inside a
- // `SharedChunk` this `Arc` holds a +1 on; `alloc_uninit_arc`
- // reserved a placeholder drop entry for it. Commit the real shim
- // so `T::drop` runs at chunk teardown.
- unsafe {
- commit_uninit_drop_entry::(self.ptr, 1, drop_entry::drop_shim::, false);
- }
- }
let thin = self.ptr;
mem::forget(self);
- // SAFETY: `thin` carries the +1 the consumed handle held; the value is
- // now a valid `T` per the caller's contract. `Arc>` and
- // `Arc` for sized `T` share the same chunk layout (no metadata
- // prefix), so no prefix rewrite is needed.
+ // SAFETY: `thin` carries the strong-count prefix and the live
+ // reference the consumed handle held; the value is now a valid
+ // `T` per the caller's contract. `MaybeUninit` and `T` share
+ // size, alignment, and (empty) metadata, so the strong-prefix
+ // chunk layout is identical and no rewrite is needed.
unsafe { Arc::from_raw(thin) }
}
@@ -198,33 +194,17 @@ impl Arc<[MaybeUninit], A> {
/// [`Arena::alloc_uninit_slice_arc`](crate::Arena::alloc_uninit_slice_arc)
/// or
/// [`Arena::alloc_zeroed_slice_arc`](crate::Arena::alloc_zeroed_slice_arc).
- ///
- /// # Panics
- ///
- /// Panics for `T: Drop` when no drop entry is found in the chunk.
#[inline]
#[must_use]
pub unsafe fn assume_init(self) -> Arc<[T], A> {
- // SAFETY: `Arc<[MaybeUninit]>` and `Arc<[T]>` share an
- // identical chunk prefix layout (the slice length, written as
- // `usize` by the allocator); read the length from the prefix
- // directly rather than relying on the (now-thin) `self.ptr`.
- let len: usize = unsafe { thin_dst::read_metadata::<[T]>(self.ptr) };
- if const { mem::needs_drop::() } {
- // SAFETY: see the scalar `assume_init`; the placeholder slice
- // drop entry reserved by `alloc_uninit_slice_arc` is committed to
- // `drop_shim::` so all `len` elements drop at chunk teardown.
- unsafe {
- commit_uninit_drop_entry::(self.ptr, len, drop_entry::drop_shim::, true);
- }
- }
let thin = self.ptr;
mem::forget(self);
- // SAFETY: `thin` carries the +1 the consumed handle held; every
- // element is now a valid `T` per the caller's contract.
- // `Arc<[MaybeUninit]>` and `Arc<[T]>` share the same chunk
- // prefix layout, so the length already stored there matches the
- // new fat pointer's metadata.
+ // SAFETY: `thin` carries the strong-count prefix and the live
+ // reference the consumed handle held; every element is now a
+ // valid `T`. `[MaybeUninit]` and `[T]` share an identical
+ // chunk prefix layout (the slice length, stored as `usize`), so
+ // the metadata already in the prefix matches the new fat
+ // pointer.
unsafe { Arc::from_raw(thin) }
}
@@ -249,67 +229,29 @@ impl Arc<[MaybeUninit], A> {
}
}
-/// Locates the placeholder [`DropEntry`](crate::internal::drop_entry) that
-/// `Arena::alloc_uninit_arc` / `alloc_uninit_slice_arc` reserved for the
-/// value at `value` and commits `drop_fn` into it, so the value's destructor
-/// runs when the hosting chunk is torn down.
-///
-/// `len` is `1` for a scalar value or the element count for a slice.
-/// `is_slice` only selects the panic message.
-///
-/// # Safety
-///
-/// - `value` must point at a value reserved via the uninit-`Arc` path, living
-/// in the first `CHUNK_ALIGN` bytes of a live `SharedChunk` on which the
-/// caller holds a strong reference.
-/// - `assume_init` must be called at most once per allocation (the placeholder
-/// commit is a non-atomic write; concurrent commits on cloned handles are
-/// not supported).
-#[inline]
-unsafe fn commit_uninit_drop_entry(value: NonNull, len: usize, drop_fn: DropFn, is_slice: bool) {
- let header = SharedChunk::::header_from_value_ptr(value);
- // SAFETY: `header` has full chunk provenance via `with_addr`;
- // reconstruct the fat DST pointer for typed field access.
- let chunk = unsafe { NonNull::new_unchecked(SharedChunk::::header_to_fat(header.as_ptr())) };
- // SAFETY: `chunk` is a live `SharedChunk` (caller holds a +1).
- let chunk_ref = unsafe { chunk.as_ref() };
- // SAFETY: `chunk` is live; `payload_ptr` returns its payload start.
- let payload = unsafe { SharedChunk::::payload_ptr(chunk) }.as_ptr();
- let payload_len = chunk_ref.capacity();
- let value_offset = (value.as_ptr() as usize) - (payload as usize);
- // Acquire pairs with the owner thread's Release publish of the count in
- // `ChunkMutator::publish_drop_count`, so the placeholder slot's bytes are
- // visible to this (possibly different) thread before we read/commit it.
- let count = chunk_ref.drop_entry_count_acquire();
- // SAFETY: `payload`, `payload_len`, and `count` describe the live chunk's
- // drop region; we hold a +1 and the contract forbids concurrent commits.
- let committed = unsafe { drop_entry::commit_placeholder_drop_fn(payload, payload_len, count, value_offset, len, drop_fn) };
- assert!(
- committed,
- "{}",
- if is_slice {
- "Arc::<[MaybeUninit]>::assume_init: no drop entry reserved for this allocation. \
- Use `Arena::alloc_uninit_slice_arc::()` / `alloc_zeroed_slice_arc`; allocating \
- a `MaybeUninit` slice via the ordinary slice-Arc helpers does not reserve one \
- and would silently leak each `T::drop`."
- } else {
- "Arc::>::assume_init: no drop entry reserved for this allocation. \
- Use `Arena::alloc_uninit_arc::()` / `alloc_zeroed_arc`; \
- `Arena::alloc_arc(MaybeUninit::new(...))` does not reserve an entry and would \
- silently leak `T::drop`."
- }
- );
+/// Saturation guard for [`Arc::clone`]: aborts the process when the
+/// strong count would overflow, mirroring `std::sync::Arc`.
+#[cfg_attr(coverage_nightly, coverage(off))]
+#[inline(never)]
+#[cold]
+fn strong_overflow_abort() -> ! {
+ refcount_overflow_abort()
}
impl Clone for Arc {
#[inline]
fn clone(&self) -> Self {
- // SAFETY: `self` owns a live +1 on its chunk so the chunk is
- // alive; `clone_from_value_ptr` mints a fresh +1 via an
- // atomic bump and returns a `ChunkRef` that owns it. We
- // `forget` that `ChunkRef`, handing the +1 to the new `Arc`.
- let chunk_ref = unsafe { ChunkRef::::clone_from_value_ptr(self.ptr) };
- let _ = chunk_ref.forget();
+ let value_align = mem::align_of_val::(&**self);
+ // SAFETY: `self` keeps the value (and its strong-count prefix)
+ // alive, so the strong slot is live, aligned, and within the
+ // chunk's provenance.
+ let strong = unsafe { thin_dst::strong_ref::(self.ptr, value_align) };
+ // Relaxed suffices (as `std::sync::Arc`): the new handle need not
+ // synchronize until it is dropped.
+ let prev = strong.fetch_add(1, Ordering::Relaxed);
+ if prev > MAX_STRONG_REFCOUNT {
+ strong_overflow_abort();
+ }
Self {
ptr: self.ptr,
_phantom: PhantomData,
@@ -320,15 +262,30 @@ impl Clone for Arc {
impl Drop for Arc {
#[inline]
fn drop(&mut self) {
- // SAFETY: `ptr` is hosted in a 64K-aligned SharedChunk we
- // hold a +1 strong reference on. `ChunkRef::from_value_ptr`
- // adopts that +1 and releases it on its own drop. We do not
- // invoke `T::drop` here — for `T: Drop`, a drop entry was
- // registered at allocation time so the chunk's teardown runs
- // `T::drop` when the last reference releases the chunk; for
- // `T: !Drop` no destructor is needed.
+ let value_align = mem::align_of_val::(&**self);
+ // SAFETY: the value (and its strong-count prefix) is still live
+ // while this handle exists; the strong slot is aligned and
+ // within chunk provenance.
+ let strong = unsafe { thin_dst::strong_ref::(self.ptr, value_align) };
+ // Release so prior accesses happen-before teardown (as `std::sync::Arc`).
+ if strong.fetch_sub(1, Ordering::Release) != 1 {
+ return;
+ }
+ // Last strong reference: Acquire-fence so other handles' writes are
+ // visible before we drop the value and release the chunk.
+ fence(Ordering::Acquire);
+ // Adopt the chunk's +1 *before* `T::drop` so a panicking destructor
+ // still releases the chunk via `ChunkRef`'s `Drop` (the in-chunk slot
+ // leaks, per the `alloc_arc*` panic semantics).
+ //
+ // SAFETY: `ptr` is hosted in a 64K-aligned `SharedChunk` that
+ // holds exactly one outstanding +1 for this whole allocation;
+ // `from_value_ptr` adopts it. The value is a valid `T` and is
+ // dropped exactly once (only on the strong → 0 transition).
unsafe {
- let _ref: ChunkRef = ChunkRef::from_value_ptr(self.ptr);
+ let _chunk: ChunkRef = ChunkRef::from_value_ptr(self.ptr);
+ let fat = self.as_fat_ptr();
+ ptr::drop_in_place(fat.as_ptr());
}
}
}
@@ -351,3 +308,72 @@ where
v.freeze_into_arc()
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::Arena;
+
+ // Pins the saturation threshold to the `u32` half-range, killing the
+ // mutant that swaps `>>` for `<<` in the constant (which would yield
+ // `0xFFFF_FFFE`). Behavioral tests cannot reach this — the boundary
+ // sits ~2 billion clones away — so assert the value directly.
+ #[test]
+ fn max_strong_refcount_is_u32_half_range() {
+ assert_eq!(MAX_STRONG_REFCOUNT, u32::MAX >> 1);
+ assert_eq!(MAX_STRONG_REFCOUNT, 0x7FFF_FFFF);
+ }
+
+ // `Arc::clone` checks `prev > MAX_STRONG_REFCOUNT` on the value
+ // returned by `fetch_add` (the count *before* the increment), so a
+ // clone observing `prev == MAX_STRONG_REFCOUNT` must NOT abort.
+ // Driving the strong count to exactly the threshold and cloning kills
+ // the `>` -> `==` and `>` -> `>=` mutants on that comparison: both
+ // would abort the process here.
+ #[test]
+ fn clone_at_max_refcount_threshold_does_not_abort() {
+ let arena = Arena::new();
+ let arc = arena.alloc_arc(0xABCD_u32);
+ // SAFETY: `arc` keeps the value and its strong-count prefix live,
+ // so the strong slot is aligned and within chunk provenance.
+ let strong = unsafe { thin_dst::strong_ref::(arc.thin_ptr(), mem::align_of::()) };
+ // Force the next clone to observe `prev == MAX_STRONG_REFCOUNT`.
+ strong.store(MAX_STRONG_REFCOUNT, Ordering::Relaxed);
+ #[expect(
+ clippy::redundant_clone,
+ reason = "exercising Arc::clone's overflow guard at the threshold is the point of the test"
+ )]
+ let clone = arc.clone();
+ assert_eq!(*clone, 0xABCD);
+ // Restore the true live-handle count (`arc` + `clone`) so the two
+ // drops tear the value and chunk down correctly instead of
+ // leaking the strong count above 1 forever.
+ strong.store(2, Ordering::Relaxed);
+ }
+
+ // A clone observing `prev > MAX_STRONG_REFCOUNT` MUST abort. Driving
+ // the strong count one past the threshold reaches the
+ // `strong_overflow_abort()` call site in `Arc::clone` (which panics
+ // instead of aborting under `cfg(test)`), covering that guard and
+ // killing the `>` -> `==` mutant (which would not fire here).
+ #[test]
+ #[should_panic(expected = "refcount overflow")]
+ fn clone_above_max_refcount_threshold_aborts() {
+ let arena = Arena::new();
+ let arc = arena.alloc_arc(0xABCD_u32);
+ // SAFETY: `arc` keeps the value and its strong-count prefix live,
+ // so the strong slot is aligned and within chunk provenance.
+ let strong = unsafe { thin_dst::strong_ref::(arc.thin_ptr(), mem::align_of::()) };
+ strong.store(MAX_STRONG_REFCOUNT + 1, Ordering::Relaxed);
+ // The clone panics in its overflow guard before returning, so no
+ // clone is produced (but `fetch_add` already bumped the count).
+ // Catch it, restore the real live-handle count (just `arc`) so
+ // teardown releases the chunk instead of leaking (keeps Miri
+ // happy), then resume so `should_panic` observes the panic.
+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ let _c = arc.clone();
+ }));
+ strong.store(1, Ordering::Relaxed);
+ std::panic::resume_unwind(result.expect_err("clone past the threshold must panic"));
+ }
+}
diff --git a/crates/multitude/src/arena/alloc_prefixed.rs b/crates/multitude/src/arena/alloc_prefixed.rs
index ee29c271f..1c2bee081 100644
--- a/crates/multitude/src/arena/alloc_prefixed.rs
+++ b/crates/multitude/src/arena/alloc_prefixed.rs
@@ -83,6 +83,15 @@ impl Arena {
// recovery invariant used by the smart pointers' `Drop`.
let payload_bytes = len.checked_mul(elem_size).ok_or(AllocError)?.max(elem_align);
let total = PREFIX_BYTES.checked_add(payload_bytes).ok_or(AllocError)?;
+ // `total` is an exact reservation size, not a worst-case hint: unlike
+ // the slice paths (which permit over-aligned `T` and so add `elem_align`
+ // of front-padding slack to their routing hint), the const-assert above
+ // bounds `elem_align <= align_of::() <= value_align`. A fresh
+ // chunk's payload base is `value_align`-aligned, so an `elem_align`
+ // reservation on a freshly refilled chunk never consumes front padding.
+ // Routing/refilling with `total` therefore always yields a chunk into
+ // which the retry's `try_alloc_with_chunk(total, elem_align)` fits — no
+ // `total` vs `total + elem_align` boundary loop is possible here.
loop {
// Allocate `total` bytes aligned to `align_of::()` so the
// payload (at offset PREFIX_BYTES, a multiple of any align
@@ -94,7 +103,7 @@ impl Arena {
let _ = chunk_ref.forget();
return Ok(payload);
}
- if self.is_oversized_shared(total) {
+ if self.is_oversized(total) {
return self.alloc_oversized_shared_with(total, |mutator, chunk_ptr| {
let (base, _chunk_unused) = mutator
.try_alloc_with_chunk(total, elem_align)
@@ -110,6 +119,69 @@ impl Arena {
}
}
+impl Arena {
+ /// Strong-prefixed [`Arc`](crate::Arc) variant of
+ /// [`Self::impl_alloc_prefixed_shared`]: reserves a per-`Arc`
+ /// [`AtomicU32`](core::sync::atomic::AtomicU32) strong count and a
+ /// `usize` length metadata word immediately before the payload,
+ /// initializes the strong count to `1`, writes the length and the
+ /// payload, takes one chunk refcount for the new `Arc` family, and
+ /// returns a thin `NonNull` to the first payload element.
+ ///
+ /// `T` must have `align_of::() <= align_of::()`; see
+ /// module docs.
+ #[inline(always)]
+ pub(crate) fn impl_alloc_prefixed_shared_arc(&self, src: &[T]) -> Result, AllocError> {
+ const {
+ assert!(
+ mem::align_of::() <= mem::align_of::(),
+ "impl_alloc_prefixed_shared_arc: T's align must not exceed usize's align",
+ );
+ }
+ let len = src.len();
+ // `src` is a live `&[T]`, so `size_of_val(src)` is a valid usize.
+ let payload_bytes = mem::size_of_val(src);
+ let bytes_needed = worst_case_arc_slice_payload::(len);
+ loop {
+ // SAFETY: `payload_bytes == size_of_val(src) == size_of::() * len`.
+ let reserved = unsafe { self.try_reserve_arc_slice_with_size::(len, payload_bytes) };
+ if let Some((uninit, chunk_ptr)) = reserved {
+ let chunk_ref: ChunkRef = self.acquire_current_shared_chunk_ref(chunk_ptr);
+ let slice_ptr = uninit.init_copy_from_slice_ptr(src);
+ let _ = chunk_ref.forget();
+ return Ok(slice_ptr.cast::());
+ }
+ if self.is_oversized(bytes_needed) {
+ return self.alloc_oversized_shared_with(bytes_needed, |mutator, chunk_ptr| {
+ let (ticket, _chunk) = mutator
+ .try_alloc_arc_slice::(len)
+ .expect("dedicated oversized chunk sized to fit prefixed Arc payload");
+ let chunk_ref: ChunkRef = acquire_shared_chunk_ref::(chunk_ptr);
+ let slice_ptr = ticket.init_copy_from_slice_ptr(src);
+ let _ = chunk_ref.forget();
+ slice_ptr.cast::()
+ });
+ }
+ self.refill_shared(bytes_needed)?;
+ }
+ }
+}
+
+/// Worst-case byte budget for a strong-prefixed `Arc` slice/prefixed
+/// payload of `len` elements: per-`Arc` strong count + slice-length
+/// prefix + payload + front alignment slack. Shared by the `Arc<[T]>`,
+/// `Arc`, and `ArcUtf16Str` allocation paths.
+#[cfg_attr(test, mutants::skip)] // underestimating refill hint ⇒ refill spin
+#[inline]
+pub(crate) fn worst_case_arc_slice_payload(len: usize) -> usize {
+ use crate::internal::thin_dst;
+ let align = mem::align_of::();
+ let value_bytes = mem::size_of::().saturating_mul(len).max(1);
+ thin_dst::strong_prefix_bytes_for(align, mem::size_of::())
+ .saturating_add(value_bytes)
+ .saturating_add(thin_dst::arc_block_align(align))
+}
+
/// Write the length prefix (unaligned `usize`) at `base` and copy
/// `src` immediately after, returning a thin pointer to the first
/// payload element.
diff --git a/crates/multitude/src/arena/alloc_slice_arc.rs b/crates/multitude/src/arena/alloc_slice_arc.rs
index c0a2b6b0c..9d6fcdcb3 100644
--- a/crates/multitude/src/arena/alloc_slice_arc.rs
+++ b/crates/multitude/src/arena/alloc_slice_arc.rs
@@ -10,7 +10,7 @@ use core::pin::Pin;
use allocator_api2::alloc::{AllocError, Allocator};
-use super::alloc_prefixed::worst_case_thin_slice_payload;
+use super::alloc_prefixed::worst_case_arc_slice_payload;
use super::alloc_value::{MAX_SMART_PTR_ALIGN, acquire_shared_chunk_ref};
use super::{Arena, ExpectAlloc};
use crate::arc::Arc;
@@ -149,34 +149,34 @@ impl Arena {
}
/// Arc + Copy: no element-drop runs, but we still take an Arc-owned
- /// refcount on the chunk.
+ /// refcount on the chunk and reserve the strong-count prefix.
#[inline]
fn impl_alloc_slice_arc_copy(&self, src: &[T]) -> Result, AllocError> {
- check_slice_arc_layout::(src.len())?;
+ check_slice_arc_layout::()?;
let len = src.len();
- // Copy is never `Drop`, so use the no-drop reservation.
- let bytes_needed = worst_case_thin_slice_payload::(len);
+ let bytes_needed = worst_case_arc_slice_payload::(len);
// `src` is a live `&[T]`, so `size_of_val(src)` is a valid
// `usize`. Hoisting the precomputed byte size lets the inner
// reservation helper skip the `checked_mul` overflow guard.
let payload_bytes = mem::size_of_val(src);
loop {
// SAFETY: `payload_bytes == size_of_val(src) == size_of::() * len`.
- let reserved = unsafe { self.try_reserve_shared_slice_with_size::(len, payload_bytes) };
+ let reserved = unsafe { self.try_reserve_arc_slice_with_size::(len, payload_bytes) };
if let Some((uninit, chunk_ptr)) = reserved {
let chunk_ref = self.acquire_current_shared_chunk_ref(chunk_ptr);
let slice_ptr = uninit.init_copy_from_slice_ptr(src);
let _ = chunk_ref.forget();
// SAFETY: `slice_ptr` points to `len` initialized `T`s in a
- // shared chunk with a fresh +1; `Arc::from_raw` adopts that
- // +1. Chunk-wide provenance preserved via `init_copy_from_slice_ptr`.
+ // shared chunk with a fresh +1 and an initialized strong
+ // prefix; `Arc::from_raw` adopts that family. Chunk-wide
+ // provenance preserved via `init_copy_from_slice_ptr`.
return Ok(unsafe { Arc::from_raw(slice_ptr.cast::()) });
}
- if self.is_oversized_shared(bytes_needed) {
+ if self.is_oversized(bytes_needed) {
return self.alloc_oversized_shared_with(bytes_needed, |mutator, chunk_ptr| {
- let ticket = mutator
- .try_alloc_uninit_slice_prefixed::(len)
- .expect("dedicated oversized chunk sized to fit slice");
+ let (ticket, _chunk) = mutator
+ .try_alloc_arc_slice::(len)
+ .expect("dedicated oversized chunk sized to fit slice + strong prefix");
let chunk_ref = acquire_shared_chunk_ref::(chunk_ptr);
let slice_ptr = ticket.init_copy_from_slice_ptr(src);
let _ = chunk_ref.forget();
@@ -188,32 +188,16 @@ impl Arena {
}
}
- /// Arc + closure fill: records a chunk drop entry when `T: Drop`,
- /// so the chunk's teardown runs `T::drop` on each element after the
- /// last `Arc` releases.
+ /// Arc + closure fill: `T::drop` (if any) runs eagerly in
+ /// [`Arc::drop`](crate::Arc) on the last reference via
+ /// `drop_in_place::<[T]>`, so no chunk drop entry is reserved.
#[inline]
fn impl_alloc_slice_arc_with T>(&self, len: usize, f: F) -> Result, AllocError> {
- check_slice_arc_layout::(len)?;
- // Refill hint accounts for the length prefix, payload alignment
- // slack, payload bytes, and (for `T: Drop`) a drop-entry slot.
- let bytes_needed = worst_case_thin_slice_payload::(len);
+ check_slice_arc_layout::()?;
+ let bytes_needed = worst_case_arc_slice_payload::(len);
let mut f = Some(f);
loop {
- // Branch on needs_drop at const time so monomorphizations
- // pick the right reservation helper.
- if const { mem::needs_drop::() } {
- if let Some((uninit, chunk_ptr)) = self.try_reserve_shared_slice_with_drop::(len) {
- let chunk_ref = self.acquire_current_shared_chunk_ref(chunk_ptr);
- let f = f.take().expect("with closure taken twice");
- let slice_ptr = uninit.init_with_ptr(f);
- let _ = chunk_ref.forget();
- // SAFETY: see `impl_alloc_slice_arc_copy`; the drop entry
- // was committed by `init_with_ptr` for the chunk-teardown
- // path. `slice_ptr` carries chunk-wide provenance so the
- // Arc's later `byte_sub` to the chunk header is sound.
- return Ok(unsafe { Arc::from_raw(slice_ptr.cast::()) });
- }
- } else if let Some((uninit, chunk_ptr)) = self.try_reserve_shared_slice::(len) {
+ if let Some((uninit, chunk_ptr)) = self.try_reserve_arc_slice::(len) {
let chunk_ref = self.acquire_current_shared_chunk_ref(chunk_ptr);
let f = f.take().expect("with closure taken twice");
let slice_ptr = uninit.init_with_ptr(f);
@@ -222,27 +206,16 @@ impl Arena {
// provenance preserved via `init_with_ptr`.
return Ok(unsafe { Arc::from_raw(slice_ptr.cast::()) });
}
- if self.is_oversized_shared(bytes_needed) {
+ if self.is_oversized(bytes_needed) {
let fclosure = f.take().expect("with closure taken twice");
return self.alloc_oversized_shared_with(bytes_needed, |mutator, chunk_ptr| {
- let slice_ptr = if const { mem::needs_drop::() } {
- let ticket = mutator
- .try_alloc_uninit_slice_with_drop_prefixed::(len)
- .expect("dedicated oversized chunk sized to fit slice + drop entry");
- let chunk_ref = acquire_shared_chunk_ref::(chunk_ptr);
- let p = ticket.init_with_ptr(fclosure);
- let _ = chunk_ref.forget();
- p
- } else {
- let ticket = mutator
- .try_alloc_uninit_slice_prefixed::(len)
- .expect("dedicated oversized chunk sized to fit slice");
- let chunk_ref = acquire_shared_chunk_ref::(chunk_ptr);
- let p = ticket.init_with_ptr(fclosure);
- let _ = chunk_ref.forget();
- p
- };
- // SAFETY: see the non-oversized branches above.
+ let (ticket, _chunk) = mutator
+ .try_alloc_arc_slice::(len)
+ .expect("dedicated oversized chunk sized to fit slice + strong prefix");
+ let chunk_ref = acquire_shared_chunk_ref::(chunk_ptr);
+ let slice_ptr = ticket.init_with_ptr(fclosure);
+ let _ = chunk_ref.forget();
+ // SAFETY: see the non-oversized branch above.
unsafe { Arc::from_raw(slice_ptr.cast::()) }
});
}
@@ -289,25 +262,15 @@ impl Arena {
}
}
-/// Common up-front checks for the `Arc<[T]>` slice family. Rejects
-/// over-aligned `T` (would break the smart-pointer header recovery) and
-/// `T: Drop` slices whose `len > u16::MAX` (the chunk drop entry packs
-/// the element count into a `u16`).
-//
-// Mutation testing is suppressed here: any mutation that bypasses the
-// `len > u16::MAX` rejection (e.g. `&&`→`||`, `>`→`==`) sends the
-// caller's refill loop into an unbounded chunk-allocation spin (see the
-// detailed note in `alloc_slice_ref::reject_drop_slice_too_long`).
-// Correctness is exercised by integration tests in `coverage_gaps.rs`,
-// `arena.rs`, and `mutants_extras.rs`.
-#[cfg_attr(test, mutants::skip)]
+/// Up-front check for the `Arc<[T]>` slice family. Rejects over-aligned
+/// `T` (would break the smart-pointer header recovery). Unlike the
+/// old drop-entry design, there is no `len > u16::MAX` restriction:
+/// element destructors run via `drop_in_place::<[T]>` in
+/// [`Arc::drop`](crate::Arc), not a `u16`-counted chunk drop entry.
#[inline]
-fn check_slice_arc_layout(len: usize) -> Result<(), AllocError> {
+fn check_slice_arc_layout() -> Result<(), AllocError> {
if mem::align_of::() >= MAX_SMART_PTR_ALIGN {
return Err(AllocError);
}
- if mem::needs_drop::() && len > u16::MAX as usize {
- return Err(AllocError);
- }
Ok(())
}
diff --git a/crates/multitude/src/arena/alloc_slice_box.rs b/crates/multitude/src/arena/alloc_slice_box.rs
index dcfa0553f..e76956411 100644
--- a/crates/multitude/src/arena/alloc_slice_box.rs
+++ b/crates/multitude/src/arena/alloc_slice_box.rs
@@ -163,19 +163,15 @@ impl Arena {
fn impl_alloc_slice_box_copy(&self, src: &[T]) -> Result, AllocError> {
check_slice_box_layout::(src.len())?;
let len = src.len();
- // `src` is a live `&[T]`, so `size_of_val(src)` is a valid
- // `usize`. Hoisting it past the refill loop spares the inner
- // reservation a `checked_mul` overflow guard.
+ // Precompute byte size so the reservation helper skips checked_mul.
let payload_bytes = mem::size_of_val(src);
let ptr = self.reserve_slice_box::(len, payload_bytes, |slot_ptr| {
// SAFETY: `slot_ptr` is the reservation start; `len` elements
// of `T` fit by construction.
unsafe { ptr::copy_nonoverlapping(src.as_ptr(), slot_ptr, len) };
})?;
- // `ptr` points to `len` initialized `T`s in a shared chunk that
- // has a fresh +1; `Box::from_raw` adopts that +1 and `Box::drop` runs
- // `drop_in_place` on the slice when the smart pointer is dropped.
- // SAFETY: see above.
+ // SAFETY: `ptr` points to `len` initialized `T`s in a shared
+ // chunk with a fresh +1; `Box::from_raw` adopts that +1.
Ok(unsafe { Box::from_raw(ptr.cast::()) })
}
@@ -184,10 +180,7 @@ impl Arena {
#[inline]
fn impl_alloc_slice_box_with T>(&self, len: usize, mut f: F) -> Result, AllocError> {
check_slice_box_layout::(len)?;
- // Caller-provided `len`: must overflow-check the payload size
- // up front so the hot loop can skip the `checked_mul`. On
- // overflow we report `AllocError` immediately rather than spin
- // refilling.
+ // Check overflow before the refill loop.
let payload_bytes = mem::size_of::().checked_mul(len).ok_or(AllocError)?;
let ptr = self.reserve_slice_box::(len, payload_bytes, |slot_ptr| {
// SAFETY: `slot_ptr` is the reservation start; we init `len` slots
@@ -243,7 +236,7 @@ impl Arena {
let _ = chunk_ref.forget();
return Ok(base);
}
- if self.is_oversized_shared(bytes_needed) {
+ if self.is_oversized(bytes_needed) {
let init_owned = init.take().expect("reserve_slice_box init taken twice");
return self.alloc_oversized_shared_with(bytes_needed, |mutator, chunk_ptr| {
let ticket = mutator
@@ -292,29 +285,18 @@ impl Arena {
}
}
-/// Common up-front checks for the `Box<[T]>` slice family. `Box::drop`
-/// runs `drop_in_place` on the entire slice eagerly, so no chunk drop
-/// entry is recorded; however we still reject `T: Drop` slices with
-/// `len > u16::MAX` so a future `Box<[T]> -> Arc<[T]>` conversion has
-/// a slot to populate (parity with the `alloc_dst_box` guard).
-//
-// Mutation testing is suppressed: bypassing the `len > u16::MAX`
-// rejection sends the caller's refill loop into an unbounded
-// chunk-allocation spin (see `alloc_slice_ref::reject_drop_slice_too_long`).
-#[cfg_attr(test, mutants::skip)]
+/// Up-front check for `Box<[T]>`: reject alignments that break
+/// smart-pointer header recovery. Slice length is full-width in the
+/// chunk prefix.
#[inline]
-fn check_slice_box_layout(len: usize) -> Result<(), AllocError> {
+fn check_slice_box_layout(_len: usize) -> Result<(), AllocError> {
if mem::align_of::() >= MAX_SMART_PTR_ALIGN {
return Err(AllocError);
}
- if mem::needs_drop::() && len > u16::MAX as usize {
- return Err(AllocError);
- }
Ok(())
}
-/// Drop-guard for partial init in `alloc_slice_*_box`. Mirrors the
-/// `InitGuard` in `internal::uninit`.
+/// Drop guard for partially initialized boxed slices.
struct InitGuard {
dst: *mut T,
initialized: usize,
diff --git a/crates/multitude/src/arena/alloc_slice_ref.rs b/crates/multitude/src/arena/alloc_slice_ref.rs
index ea96a67e3..ee0ee4d4a 100644
--- a/crates/multitude/src/arena/alloc_slice_ref.rs
+++ b/crates/multitude/src/arena/alloc_slice_ref.rs
@@ -290,7 +290,7 @@ impl Arena {
#[cfg_attr(test, mutants::skip)]
fn refill_or_alloc_oversized_slice_copy(&self, src: &[T]) -> Result