Skip to content

Commit ada14e6

Browse files
committed
feat: slab-friendly XNode layout, refactor XEntry, add benchmarks
- Add optional `slab-friendly` feature: when enabled, XNode slots are heap-allocated via Box, changing allocation footprint for slab allocators (may reduce internal fragmentation; allocator-dependent). - Pin smallvec to 1.13.1 for reproducibility. - Simplify XEntry::ty() with explicit if-else; add panic message for invalid XEntry tags; remove unused Not import. - Add benches/xarray_bench.rs for cargo bench (store, cursor load, random load, range, COW clone+overwrite). Signed-off-by: longjin <longjin@dragonos.org>
1 parent 1dad5d9 commit ada14e6

5 files changed

Lines changed: 203 additions & 8 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9-
smallvec = "*"
9+
smallvec = "1.13.1"
1010

1111
[features]
1212
default = []
1313
std = []
14+
# Optimize memory layout for slab allocators, reducing memory usage
15+
slab-friendly = []

benches/xarray_bench.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#![feature(test)]
2+
3+
extern crate test;
4+
5+
use std::sync::Arc;
6+
7+
use test::{black_box, Bencher};
8+
use xarray::XArray;
9+
10+
const DENSE_LEN: u64 = 100_000;
11+
const SPARSE_LEN: u64 = 200_000;
12+
const RANDOM_QUERIES: usize = 50_000;
13+
const COW_WRITE_COUNT: u64 = 20_000;
14+
15+
fn build_dense(len: u64) -> XArray<Arc<u64>> {
16+
let mut xa: XArray<Arc<u64>> = XArray::new();
17+
for i in 0..len {
18+
xa.store(i, Arc::new(i));
19+
}
20+
xa
21+
}
22+
23+
fn random_indices(len: u64, count: usize) -> Vec<u64> {
24+
let mut seed = 0x1234_5678_9abc_def0_u64;
25+
let mut out = Vec::with_capacity(count);
26+
for _ in 0..count {
27+
seed ^= seed << 7;
28+
seed ^= seed >> 9;
29+
seed ^= seed << 8;
30+
out.push(seed % len);
31+
}
32+
out
33+
}
34+
35+
#[bench]
36+
fn bench_store_dense(b: &mut Bencher) {
37+
b.iter(|| {
38+
let mut xa: XArray<Arc<u64>> = XArray::new();
39+
for i in 0..DENSE_LEN {
40+
xa.store(i, Arc::new(i));
41+
}
42+
black_box(xa);
43+
});
44+
}
45+
46+
#[bench]
47+
fn bench_cursor_load_dense(b: &mut Bencher) {
48+
let xa = build_dense(DENSE_LEN);
49+
50+
b.iter(|| {
51+
let mut cursor = xa.cursor(0);
52+
let mut sum = 0_u64;
53+
for _ in 0..DENSE_LEN {
54+
if let Some(v) = cursor.load() {
55+
sum = sum.wrapping_add(*v.as_ref());
56+
}
57+
cursor.next();
58+
}
59+
black_box(sum);
60+
});
61+
}
62+
63+
#[bench]
64+
fn bench_load_random_dense(b: &mut Bencher) {
65+
let xa = build_dense(DENSE_LEN);
66+
let indices = random_indices(DENSE_LEN, RANDOM_QUERIES);
67+
68+
b.iter(|| {
69+
let mut sum = 0_u64;
70+
for &idx in &indices {
71+
if let Some(v) = xa.load(idx) {
72+
sum = sum.wrapping_add(*v.as_ref());
73+
}
74+
}
75+
black_box(sum);
76+
});
77+
}
78+
79+
#[bench]
80+
fn bench_range_sparse_even(b: &mut Bencher) {
81+
let mut xa: XArray<Arc<u64>> = XArray::new();
82+
for i in 0..SPARSE_LEN {
83+
if i % 2 == 0 {
84+
xa.store(i, Arc::new(i));
85+
}
86+
}
87+
88+
b.iter(|| {
89+
let mut sum = 0_u64;
90+
let mut cnt = 0_u64;
91+
for (index, item) in xa.range(0..SPARSE_LEN) {
92+
sum = sum.wrapping_add(index).wrapping_add(*item.as_ref());
93+
cnt += 1;
94+
}
95+
black_box((sum, cnt));
96+
});
97+
}
98+
99+
#[bench]
100+
fn bench_cow_clone_then_overwrite(b: &mut Bencher) {
101+
let xa = build_dense(DENSE_LEN);
102+
103+
b.iter(|| {
104+
let mut cloned = xa.clone();
105+
for i in 0..COW_WRITE_COUNT {
106+
let idx = (i * 7) % DENSE_LEN;
107+
cloned.store(idx, Arc::new(i));
108+
}
109+
black_box(cloned.load(0));
110+
});
111+
}

src/entry.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use alloc::boxed::Box;
22
use alloc::sync::Arc;
33
use core::marker::PhantomData;
44
use core::mem::ManuallyDrop;
5-
use core::ops::{Deref, Not};
5+
use core::ops::Deref;
66

77
use crate::node::{TryClone, XNode};
88

@@ -67,7 +67,10 @@ impl<'a, T> Deref for ArcRef<'a, T> {
6767

6868
// SAFETY: `Arc<T>` meets the safety requirements of `ItemEntry`.
6969
unsafe impl<T> ItemEntry for Arc<T> {
70-
type Ref<'a> = ArcRef<'a, T> where Self: 'a;
70+
type Ref<'a>
71+
= ArcRef<'a, T>
72+
where
73+
Self: 'a;
7174

7275
fn into_raw(self) -> *const () {
7376
// A contant expression, so compilers should be smart enough to optimize it away.
@@ -118,7 +121,10 @@ impl<'a, T> Deref for BoxRef<'a, T> {
118121

119122
// SAFETY: `Box<T>` meets the safety requirements of `ItemEntry`.
120123
unsafe impl<T> ItemEntry for Box<T> {
121-
type Ref<'a> = BoxRef<'a, T> where Self: 'a;
124+
type Ref<'a>
125+
= BoxRef<'a, T>
126+
where
127+
Self: 'a;
122128

123129
fn into_raw(self) -> *const () {
124130
// A contant expression, so compilers should be smart enough to optimize it away.
@@ -214,9 +220,16 @@ impl<I: ItemEntry> XEntry<I> {
214220
}
215221

216222
fn ty(&self) -> Option<EntryType> {
217-
self.is_null()
218-
.not()
219-
.then(|| (self.raw.addr() & Self::TYPE_MASK).try_into().unwrap())
223+
if self.is_null() {
224+
return None;
225+
}
226+
227+
let raw = self.raw.addr();
228+
let tag = raw & Self::TYPE_MASK;
229+
match tag.try_into() {
230+
Ok(ty) => Some(ty),
231+
Err(()) => panic!("xarray: invalid XEntry tag={tag:#x}, raw={raw:#x}"),
232+
}
220233
}
221234

222235
pub fn is_null(&self) -> bool {

src/node.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use core::cmp::Ordering;
2+
#[cfg(feature = "slab-friendly")]
3+
use core::convert::TryInto;
24
use core::ops::{Deref, DerefMut};
35

46
use crate::entry::{ItemEntry, XEntry};
@@ -103,7 +105,10 @@ where
103105
offset_in_parent: u8,
104106
/// The slots storing `XEntry`s, which point to user-given items for leaf nodes and other
105107
/// `XNode`s for interior nodes.
108+
#[cfg(not(feature = "slab-friendly"))]
106109
slots: [XEntry<I>; SLOT_SIZE],
110+
#[cfg(feature = "slab-friendly")]
111+
slots: alloc::boxed::Box<[XEntry<I>; SLOT_SIZE]>,
107112
/// The marks representing whether each slot is marked or not.
108113
///
109114
/// Users can set mark or unset mark on user-given items, and a leaf node or an interior node
@@ -112,6 +117,16 @@ where
112117
}
113118

114119
impl<I: ItemEntry> XNode<I> {
120+
#[cfg(feature = "slab-friendly")]
121+
fn empty_slots() -> alloc::boxed::Box<[XEntry<I>; SLOT_SIZE]> {
122+
let mut slots = alloc::vec::Vec::with_capacity(SLOT_SIZE);
123+
slots.resize_with(SLOT_SIZE, || XEntry::EMPTY);
124+
match slots.into_boxed_slice().try_into() {
125+
Ok(slots) => slots,
126+
Err(_) => panic!("xarray: invalid slab-friendly slot length"),
127+
}
128+
}
129+
115130
pub fn new_root(height: Height) -> Self {
116131
Self::new(height, 0)
117132
}
@@ -120,7 +135,10 @@ impl<I: ItemEntry> XNode<I> {
120135
Self {
121136
height,
122137
offset_in_parent: offset,
138+
#[cfg(not(feature = "slab-friendly"))]
123139
slots: [XEntry::EMPTY; SLOT_SIZE],
140+
#[cfg(feature = "slab-friendly")]
141+
slots: Self::empty_slots(),
124142
marks: [Mark::EMPTY; NUM_MARKS],
125143
}
126144
}
@@ -147,7 +165,10 @@ impl<I: ItemEntry> XNode<I> {
147165
}
148166

149167
pub fn entries_mut(&mut self) -> &mut [XEntry<I>] {
150-
&mut self.slots
168+
#[cfg(not(feature = "slab-friendly"))]
169+
return &mut self.slots;
170+
#[cfg(feature = "slab-friendly")]
171+
return self.slots.as_mut();
151172
}
152173

153174
pub fn is_marked(&self, offset: u8, mark: usize) -> bool {

src/test.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,54 @@ fn test_store_overwrite() {
7474
assert_eq!(*v.as_ref(), 40);
7575
}
7676

77+
#[cfg(feature = "slab-friendly")]
78+
#[test]
79+
fn test_slab_friendly_store_overwrite_remove() {
80+
#[derive(Debug, Clone, PartialEq, Eq)]
81+
struct Token(u32);
82+
83+
let mut xa: XArray<Arc<Token>> = XArray::new();
84+
let shared = Arc::new(Token(7));
85+
86+
for i in 1..n!(256) {
87+
xa.store(i as u64, shared.clone());
88+
}
89+
for i in 1..n!(256) {
90+
assert_eq!(xa.load(i as u64).as_deref(), Some(&shared));
91+
}
92+
93+
for i in 1..n!(256) {
94+
xa.store(i as u64, Arc::new(Token((i % 251) as u32)));
95+
}
96+
for i in 1..n!(256) {
97+
assert_eq!(xa.load(i as u64).unwrap().0, (i % 251) as u32);
98+
}
99+
100+
for i in 1..n!(256) {
101+
assert!(xa.remove(i as u64).is_some());
102+
assert!(xa.load(i as u64).is_none());
103+
}
104+
}
105+
106+
#[cfg(feature = "slab-friendly")]
107+
#[test]
108+
fn test_slab_friendly_cow_overwrite() {
109+
let mut xa: XArray<Arc<u64>> = XArray::new();
110+
for i in 1..n!(128) {
111+
xa.store(i as u64, Arc::new(i));
112+
}
113+
114+
let mut cloned = xa.clone();
115+
for i in 1..n!(128) {
116+
cloned.store(i as u64, Arc::new(i * 3));
117+
}
118+
119+
for i in 1..n!(128) {
120+
assert_eq!(*xa.load(i as u64).unwrap().as_ref(), i);
121+
assert_eq!(*cloned.load(i as u64).unwrap().as_ref(), i * 3);
122+
}
123+
}
124+
77125
#[test]
78126
fn test_remove() {
79127
let mut xarray_arc: XArray<Arc<i32>> = XArray::new();

0 commit comments

Comments
 (0)