Skip to content

Commit b71c8e4

Browse files
geeknoidMartin Taillefer
andauthored
refactor(multitude)!: unify local and shared chunks into one chunk type (#516)
- Collapse the two-chunk-type design into a single unified chunk, simplifying the allocator code considerably and enabling O(1) conversions of vectors and strings into arcs and boxes. - Replace the dedicated `ArcUtf16Str`/`BoxUtf16Str` types with a transparent `Utf16Str` newtype to get much better composaibility. Co-authored-by: Martin Taillefer <mataille@microsoft.com>
1 parent 4bc01ff commit b71c8e4

88 files changed

Lines changed: 3657 additions & 4069 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.spelling

Lines changed: 68 additions & 64 deletions
Large diffs are not rendered by default.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ fundle_macros = { path = "crates/fundle_macros", default-features = false, versi
4545
fundle_macros_impl = { path = "crates/fundle_macros_impl", default-features = false, version = "0.3.3" }
4646
http_extensions = { path = "crates/http_extensions", default-features = false, version = "0.6.2" }
4747
layered = { path = "crates/layered", default-features = false, version = "0.3.4" }
48-
multitude = { path = "crates/multitude", default-features = false, version = "0.3.2" }
48+
multitude = { path = "crates/multitude", default-features = false, version = "0.4.0" }
4949
ohno = { path = "crates/ohno", default-features = false, version = "0.3.6" }
5050
ohno_macros = { path = "crates/ohno_macros", default-features = false, version = "0.3.4" }
5151
recoverable = { path = "crates/recoverable", default-features = false, version = "0.1.6" }

crates/multitude/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
[package]
55
name = "multitude"
6-
version = "0.3.2"
6+
version = "0.4.0"
77
description = "Fast and flexible arena allocator."
88
readme = "README.md"
99
keywords = ["arena", "memory", "allocator", "bump"]

crates/multitude/README.md

Lines changed: 127 additions & 136 deletions
Large diffs are not rendered by default.

crates/multitude/benches/criterion_alloc.rs

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,59 +28,54 @@ const SLICE_LEN: usize = 8;
2828
/// **and fully primed** so the timed region exercises only the warm
2929
/// bump cursor — no cold `refill_*` call on the first inner iteration.
3030
///
31-
/// 1. `with_capacity_local(64 KiB) + with_capacity_shared(64 KiB)` pin
32-
/// the very first chunk for each flavor to the largest size class.
33-
/// The arena's adaptive `1 KiB → 64 KiB` ramp would otherwise call
34-
/// into the system allocator several times growing through the
35-
/// smaller classes during the timed region.
36-
/// 2. The arena's `current_local` / `current_shared` slots start in
37-
/// the empty-mutator state (lazy first-chunk install — see the
38-
/// design doc). A single throwaway allocation of each flavor pops
39-
/// the preallocated chunk from the provider cache and installs it
40-
/// in the `current_*` slot, so the timed region's very first
41-
/// allocation hits the warm bump path. This matches bumpalo's
42-
/// `warm_bump` (which similarly primes its cursor with a no-op
43-
/// alloc) and isolates the comparison to "in-chunk bump cost
44-
/// only" with no cold refill amortized into the per-op number.
31+
/// 1. `with_capacity(128 KiB)` pins the first chunks to the largest
32+
/// size class. The arena's adaptive `1 KiB → 64 KiB` ramp would
33+
/// otherwise call into the system allocator several times growing
34+
/// through the smaller classes during the timed region.
35+
/// 2. The arena's `current` slot starts in the empty-mutator state
36+
/// (lazy first-chunk install — see the design doc). A throwaway
37+
/// reference allocation and a throwaway `Arc` allocation pop the
38+
/// preallocated chunk(s) from the provider cache and install one in
39+
/// the `current` slot, so the timed region's very first allocation
40+
/// hits the warm bump path. This matches bumpalo's `warm_bump`
41+
/// (which similarly primes its cursor with a no-op alloc) and
42+
/// isolates the comparison to "in-chunk bump cost only" with no cold
43+
/// refill amortized into the per-op number.
4544
///
46-
/// Only used by benches that exercise **both** flavors in the timed
47-
/// region: the `vec_builder` benches build a local `Vec` and then
48-
/// `into_arc()` it. Benches that touch a single flavor must use
49-
/// [`warm_arena_local`] / [`warm_arena_shared`] instead: priming the
50-
/// unused flavor allocates a dead-weight 64 KiB chunk that doubles the
51-
/// arena's memory footprint and inflates the measured per-op time
52-
/// through extra cache/TLB pressure at the batch working-set sizes
53-
/// criterion picks (verified: priming the unused shared chunk made
54-
/// `alloc_slice_copy` measure ~9x slower than its true cost).
45+
/// Only used by benches that allocate **both** references and smart
46+
/// pointers in the timed region: the `vec_builder` benches build a
47+
/// `Vec` and then `into_arc()` it. Benches that touch only one style
48+
/// must use [`warm_arena_local`] / [`warm_arena_shared`] instead:
49+
/// preallocating the extra 64 KiB doubles the arena's memory footprint
50+
/// and inflates the measured per-op time through extra cache/TLB
51+
/// pressure at the batch working-set sizes criterion picks (verified:
52+
/// the extra primed chunk made `alloc_slice_copy` measure ~9x slower
53+
/// than its true cost).
5554
fn warm_arena() -> Arena {
56-
let arena = Arena::builder()
57-
.with_capacity_local(64 * 1024)
58-
.with_capacity_shared(64 * 1024)
59-
.build();
55+
let arena = Arena::builder().with_capacity(128 * 1024).build();
6056
let _: &mut u64 = arena.alloc(0_u64);
6157
let _ = arena.alloc_arc(0_u64);
6258
arena
6359
}
6460

65-
/// Like [`warm_arena`] but primes **only** the local (ref/value)
66-
/// flavor. Used by benches whose timed region allocates exclusively
67-
/// from the local chunk (`alloc`, `alloc_str`, `alloc_slice_*` ref,
68-
/// `alloc_string*`). Priming the shared flavor here would add a
69-
/// dead-weight 64 KiB chunk — see [`warm_arena`].
61+
/// Like [`warm_arena`] but preallocates a single 64 KiB chunk and
62+
/// primes it with a reference allocation. Used by benches whose timed
63+
/// region allocates only references (`alloc`, `alloc_str`,
64+
/// `alloc_slice_*` ref, `alloc_string*`). Preallocating more would add
65+
/// a dead-weight 64 KiB chunk — see [`warm_arena`].
7066
fn warm_arena_local() -> Arena {
71-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
67+
let arena = Arena::builder().with_capacity(64 * 1024).build();
7268
let _: &mut u64 = arena.alloc(0_u64);
7369
arena
7470
}
7571

76-
/// Like [`warm_arena`] but primes **only** the shared (`Box`/`Arc`)
77-
/// flavor. Used by benches whose timed region allocates exclusively
78-
/// from the shared chunk (every `*_box` / `*_arc` bench — both smart
79-
/// pointers are backed by refcounted shared chunks). Priming the local
80-
/// flavor here would add a dead-weight 64 KiB chunk — see
81-
/// [`warm_arena`].
72+
/// Like [`warm_arena`] but preallocates a single 64 KiB chunk and
73+
/// primes it with an `Arc` allocation. Used by benches whose timed
74+
/// region allocates only smart pointers (every `*_box` / `*_arc`
75+
/// bench). Preallocating more would add a dead-weight 64 KiB chunk —
76+
/// see [`warm_arena`].
8277
fn warm_arena_shared() -> Arena {
83-
let arena = Arena::builder().with_capacity_shared(64 * 1024).build();
78+
let arena = Arena::builder().with_capacity(64 * 1024).build();
8479
let _ = arena.alloc_arc(0_u64);
8580
arena
8681
}

crates/multitude/benches/criterion_arc_array.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fn build_arena(arena: &Arena, payload: &[u8]) -> ArenaArc<[ArenaArc<[u8]>]> {
4747
for _ in 0..PROPERTIES {
4848
properties.push(arena.alloc_slice_copy_arc(payload));
4949
}
50-
properties.try_into_arc().unwrap()
50+
properties.try_into_arc_slice().unwrap()
5151
}
5252

5353
fn build_arena_from_slice(arena: &Arena, properties: &[StdArc<[u8]>]) -> ArenaArc<[StdArc<[u8]>]> {

crates/multitude/benches/criterion_drop.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,23 @@ fn bench_drop(c: &mut Criterion) {
5353
}
5454

5555
drop_bench!("box_u64", {
56-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
56+
let arena = Arena::builder().with_capacity(64 * 1024).build();
5757
let mut h = Vec::with_capacity(N);
5858
for i in 0..N {
5959
h.push(arena.alloc_box(i as u64));
6060
}
6161
(h, arena)
6262
});
6363
drop_bench!("rc_u64", {
64-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
64+
let arena = Arena::builder().with_capacity(64 * 1024).build();
6565
let mut h = Vec::with_capacity(N);
6666
for i in 0..N {
6767
h.push(arena.alloc_arc(i as u64));
6868
}
6969
(h, arena)
7070
});
7171
drop_bench!("arc_u64", {
72-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
72+
let arena = Arena::builder().with_capacity(64 * 1024).build();
7373
let mut h = Vec::with_capacity(N);
7474
for i in 0..N {
7575
h.push(arena.alloc_arc(i as u64));
@@ -78,23 +78,23 @@ fn bench_drop(c: &mut Criterion) {
7878
});
7979

8080
drop_bench!("box_droppy", {
81-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
81+
let arena = Arena::builder().with_capacity(64 * 1024).build();
8282
let mut h = Vec::with_capacity(N);
8383
for i in 0..N {
8484
h.push(arena.alloc_box(make_droppy(i)));
8585
}
8686
(h, arena)
8787
});
8888
drop_bench!("rc_droppy", {
89-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
89+
let arena = Arena::builder().with_capacity(64 * 1024).build();
9090
let mut h = Vec::with_capacity(N);
9191
for i in 0..N {
9292
h.push(arena.alloc_arc(make_droppy(i)));
9393
}
9494
(h, arena)
9595
});
9696
drop_bench!("arc_droppy", {
97-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
97+
let arena = Arena::builder().with_capacity(64 * 1024).build();
9898
let mut h = Vec::with_capacity(N);
9999
for i in 0..N {
100100
h.push(arena.alloc_arc(make_droppy(i)));
@@ -103,23 +103,23 @@ fn bench_drop(c: &mut Criterion) {
103103
});
104104

105105
drop_bench!("str_box", {
106-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
106+
let arena = Arena::builder().with_capacity(64 * 1024).build();
107107
let mut h: Vec<Box<str>> = Vec::with_capacity(N);
108108
for i in 0..N {
109109
h.push(arena.alloc_str_box(format!("word{i}")));
110110
}
111111
(h, arena)
112112
});
113113
drop_bench!("str_rc", {
114-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
114+
let arena = Arena::builder().with_capacity(64 * 1024).build();
115115
let mut h: Vec<Arc<str>> = Vec::with_capacity(N);
116116
for i in 0..N {
117117
h.push(arena.alloc_str_arc(format!("word{i}")));
118118
}
119119
(h, arena)
120120
});
121121
drop_bench!("str_arc", {
122-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
122+
let arena = Arena::builder().with_capacity(64 * 1024).build();
123123
let mut h: Vec<Arc<str>> = Vec::with_capacity(N);
124124
for i in 0..N {
125125
h.push(arena.alloc_str_arc(format!("word{i}")));
@@ -128,23 +128,23 @@ fn bench_drop(c: &mut Criterion) {
128128
});
129129

130130
drop_bench!("slice_box_u64", {
131-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
131+
let arena = Arena::builder().with_capacity(64 * 1024).build();
132132
let mut h: Vec<Box<[u64]>> = Vec::with_capacity(N);
133133
for _ in 0..N {
134134
h.push(arena.alloc_slice_fill_with_box::<u64, _>(SLICE_LEN, |j| j as u64));
135135
}
136136
(h, arena)
137137
});
138138
drop_bench!("slice_rc_u64", {
139-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
139+
let arena = Arena::builder().with_capacity(64 * 1024).build();
140140
let mut h: Vec<Arc<[u64]>> = Vec::with_capacity(N);
141141
for _ in 0..N {
142142
h.push(arena.alloc_slice_fill_with_arc::<u64, _>(SLICE_LEN, |j| j as u64));
143143
}
144144
(h, arena)
145145
});
146146
drop_bench!("slice_arc_u64", {
147-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
147+
let arena = Arena::builder().with_capacity(64 * 1024).build();
148148
let mut h: Vec<Arc<[u64]>> = Vec::with_capacity(N);
149149
for _ in 0..N {
150150
h.push(arena.alloc_slice_fill_with_arc::<u64, _>(SLICE_LEN, |j| j as u64));
@@ -153,23 +153,23 @@ fn bench_drop(c: &mut Criterion) {
153153
});
154154

155155
drop_bench!("slice_box_droppy", {
156-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
156+
let arena = Arena::builder().with_capacity(64 * 1024).build();
157157
let mut h: Vec<Box<[DroppyT]>> = Vec::with_capacity(N);
158158
for _ in 0..N {
159159
h.push(arena.alloc_slice_fill_with_box::<DroppyT, _>(SLICE_LEN, make_droppy));
160160
}
161161
(h, arena)
162162
});
163163
drop_bench!("slice_rc_droppy", {
164-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
164+
let arena = Arena::builder().with_capacity(64 * 1024).build();
165165
let mut h: Vec<Arc<[DroppyT]>> = Vec::with_capacity(N);
166166
for _ in 0..N {
167167
h.push(arena.alloc_slice_fill_with_arc::<DroppyT, _>(SLICE_LEN, make_droppy));
168168
}
169169
(h, arena)
170170
});
171171
drop_bench!("slice_arc_droppy", {
172-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
172+
let arena = Arena::builder().with_capacity(64 * 1024).build();
173173
let mut h: Vec<Arc<[DroppyT]>> = Vec::with_capacity(N);
174174
for _ in 0..N {
175175
h.push(arena.alloc_slice_fill_with_arc::<DroppyT, _>(SLICE_LEN, make_droppy));
@@ -178,7 +178,7 @@ fn bench_drop(c: &mut Criterion) {
178178
});
179179

180180
drop_bench!("alloc", {
181-
let arena = Arena::builder().with_capacity_local(64 * 1024).build();
181+
let arena = Arena::builder().with_capacity(64 * 1024).build();
182182
for i in 0..N {
183183
let _: &mut u64 = arena.alloc(i as u64);
184184
}

crates/multitude/benches/gungraun_alloc/linux.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,19 @@ fn warm_bump() -> bumpalo::Bump {
6161
}
6262

6363
fn warm_arena() -> Arena {
64-
// Warm: preallocate one chunk of the largest size class for each
65-
// flavor AND prime the arena's current_local / current_shared
66-
// mutators by performing a throwaway allocation of each flavor.
64+
// Warm: preallocate chunks of the largest size class AND prime the
65+
// arena's `current` mutator by performing a throwaway reference
66+
// allocation and a throwaway `Arc` allocation.
6767
// The preallocated chunks live in the provider cache; the
68-
// current_* slots start in the empty-mutator state and are only
68+
// `current` slot starts in the empty-mutator state and is only
6969
// populated lazily on the first allocation. Without the prime,
70-
// every bench fn entry would pay one cold `refill_*` (chunk-cache
70+
// every bench fn entry would pay one cold `refill` (chunk-cache
7171
// pop + mutator install) on the first inner iteration, hiding
7272
// ~50 cold-path instructions inside the per-op instruction count
7373
// and adding cache-miss latency that doesn't reflect steady-state
7474
// performance. This mirrors bumpalo's `warm_bump` (which itself
7575
// primes its cursor with a no-op alloc).
76-
let arena = Arena::builder()
77-
.with_capacity_local(64 * 1024)
78-
.with_capacity_shared(64 * 1024)
79-
.build();
76+
let arena = Arena::builder().with_capacity(128 * 1024).build();
8077
let _: &mut u64 = arena.alloc(0_u64);
8178
let _ = arena.alloc_arc(0_u64);
8279
arena

crates/multitude/benches/gungraun_arc_array/linux.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ fn build_arena(arena: &Arena, payload: &[u8]) -> ArenaArrayOfArena {
6868
for _ in 0..PROPERTIES {
6969
properties.push(arena.alloc_slice_copy_arc(payload));
7070
}
71-
properties.try_into_arc().unwrap()
71+
properties.try_into_arc_slice().unwrap()
7272
}
7373

7474
fn build_arena_from_slice(arena: &Arena, properties: &[StdArc<[u8]>]) -> ArenaArrayOfGlobal {
@@ -87,14 +87,11 @@ fn global_properties() -> Vec<StdArc<[u8]>> {
8787
}
8888

8989
fn warm_arena() -> Arena {
90-
// Warm: preallocate one chunk of the largest size class for each flavor
91-
// AND prime the arena's current_local / current_shared mutators with a
92-
// throwaway allocation, so the timed body never pays a cold `refill_*`.
93-
// Mirrors `gungraun_alloc::warm_arena`.
94-
let arena = Arena::builder()
95-
.with_capacity_local(64 * 1024)
96-
.with_capacity_shared(64 * 1024)
97-
.build();
90+
// Warm: preallocate chunks of the largest size class AND prime the
91+
// arena's `current` mutator with a throwaway reference allocation
92+
// and a throwaway `Arc` allocation, so the timed body never pays a
93+
// cold `refill`. Mirrors `gungraun_alloc::warm_arena`.
94+
let arena = Arena::builder().with_capacity(128 * 1024).build();
9895
let _: &mut u64 = arena.alloc(0_u64);
9996
let _ = arena.alloc_arc(0_u64);
10097
arena

0 commit comments

Comments
 (0)