1use yo_common::{Addr, CACHE_LINE};
20
21pub const SLOTS: usize = 7;
23
24pub const EMPTY: u8 = 0;
26
27const FLAG_OVERFLOW: u8 = 1;
29
30const ONES: u64 = 0x0101_0101_0101_0101;
32const LOW7: u64 = 0x7f7f_7f7f_7f7f_7f7f;
34const LANES: u64 = 0x0080_8080_8080_8080;
37
38#[repr(C, align(64))]
40#[derive(Clone, Copy)]
41pub struct Bucket {
42 tags: [u8; SLOTS],
43 flags: u8,
44 addrs: [[u8; 7]; SLOTS],
45 link: [u8; 7],
46}
47
48const _: () = {
49 assert!(size_of::<Bucket>() == CACHE_LINE);
50 assert!(align_of::<Bucket>() == CACHE_LINE);
51};
52
53impl Default for Bucket {
54 fn default() -> Bucket {
55 Bucket::EMPTY
56 }
57}
58
59impl Bucket {
60 pub const EMPTY: Bucket = Bucket {
62 tags: [EMPTY; SLOTS],
63 flags: 0,
64 addrs: [[0; 7]; SLOTS],
65 link: [0; 7],
66 };
67
68 #[inline(always)]
70 fn tag_word(&self) -> u64 {
71 let base: *const u8 = core::ptr::from_ref(self).cast();
78 unsafe { base.cast::<u64>().read_unaligned().to_le() }
83 }
84
85 #[inline(always)]
90 pub fn match_tag(&self, tag: u8) -> SlotMask {
91 let word = self.tag_word();
92 let x = word ^ (ONES.wrapping_mul(tag as u64));
93 let z = !((x & LOW7).wrapping_add(LOW7) | x | LOW7);
102 SlotMask(z & LANES)
103 }
104
105 #[inline(always)]
107 pub fn match_empty(&self) -> SlotMask {
108 self.match_tag(EMPTY)
109 }
110
111 #[inline(always)]
113 pub fn is_full(&self) -> bool {
114 self.match_empty().is_empty()
115 }
116
117 #[inline(always)]
119 pub fn tag(&self, i: usize) -> u8 {
120 self.tags[i]
121 }
122
123 #[inline(always)]
125 pub fn addr(&self, i: usize) -> Addr {
126 Addr::from_bits(read56(&self.addrs[i]))
127 }
128
129 #[inline(always)]
135 pub fn set(&mut self, i: usize, tag: u8, addr: Addr) {
136 assert_ne!(tag, EMPTY, "an occupied slot cannot carry the empty tag");
137 self.tags[i] = tag;
138 write56(&mut self.addrs[i], addr.to_bits());
139 }
140
141 #[inline(always)]
143 pub fn set_addr(&mut self, i: usize, addr: Addr) {
144 debug_assert_ne!(self.tags[i], EMPTY);
145 write56(&mut self.addrs[i], addr.to_bits());
146 }
147
148 #[inline(always)]
154 pub fn clear(&mut self, i: usize) {
155 self.tags[i] = EMPTY;
156 self.addrs[i] = [0; 7];
157 }
158
159 #[inline(always)]
161 pub fn link(&self) -> Option<u64> {
162 if self.flags & FLAG_OVERFLOW == 0 {
163 return None;
164 }
165 Some(read56(&self.link))
166 }
167
168 #[inline(always)]
170 pub fn has_overflow(&self) -> bool {
171 self.flags & FLAG_OVERFLOW != 0
172 }
173
174 #[inline]
176 pub fn set_link(&mut self, target: u64) {
177 write56(&mut self.link, target);
178 self.flags |= FLAG_OVERFLOW;
179 }
180
181 #[inline]
183 pub fn clear_link(&mut self) {
184 self.link = [0; 7];
185 self.flags &= !FLAG_OVERFLOW;
186 }
187
188 pub fn occupancy(&self) -> u32 {
190 self.tags.iter().filter(|&&t| t != EMPTY).count() as u32
191 }
192}
193
194impl core::fmt::Debug for Bucket {
195 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
196 f.debug_struct("Bucket")
197 .field("tags", &self.tags)
198 .field("occupancy", &self.occupancy())
199 .field("link", &self.link())
200 .finish()
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub struct SlotMask(u64);
207
208impl SlotMask {
209 #[inline(always)]
211 pub const fn is_empty(self) -> bool {
212 self.0 == 0
213 }
214
215 #[inline(always)]
217 pub const fn first(self) -> Option<usize> {
218 if self.0 == 0 {
219 None
220 } else {
221 Some((self.0.trailing_zeros() / 8) as usize)
222 }
223 }
224
225 #[inline(always)]
227 pub const fn count(self) -> u32 {
228 self.0.count_ones()
229 }
230}
231
232impl Iterator for SlotMask {
233 type Item = usize;
234
235 #[inline(always)]
236 fn next(&mut self) -> Option<usize> {
237 if self.0 == 0 {
238 return None;
239 }
240 let i = (self.0.trailing_zeros() / 8) as usize;
241 self.0 &= self.0 - 1;
242 Some(i)
243 }
244}
245
246#[inline(always)]
247fn read56(b: &[u8; 7]) -> u64 {
248 u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], 0])
254}
255
256#[inline(always)]
257fn write56(b: &mut [u8; 7], v: u64) {
258 let x = v.to_le_bytes();
259 b.copy_from_slice(&x[..7]);
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use yo_common::Space;
266
267 #[test]
268 fn a_bucket_is_one_cache_line() {
269 assert_eq!(size_of::<Bucket>(), 64);
270 assert_eq!(align_of::<Bucket>(), 64);
271 }
272
273 #[test]
274 fn field_offsets_match_the_specification() {
275 let b = Bucket::EMPTY;
276 let base = (&b as *const Bucket).addr();
277 assert_eq!((&raw const b.tags).addr() - base, 0);
278 assert_eq!((&raw const b.flags).addr() - base, 7);
279 assert_eq!((&raw const b.addrs).addr() - base, 8);
280 assert_eq!((&raw const b.link).addr() - base, 57);
281 }
282
283 #[test]
284 fn an_empty_bucket_matches_nothing_and_is_all_free() {
285 let b = Bucket::EMPTY;
286 assert!(b.match_tag(1).is_empty());
287 assert!(b.match_tag(255).is_empty());
288 assert_eq!(b.match_empty().count(), SLOTS as u32);
289 assert!(!b.is_full());
290 }
291
292 #[test]
293 fn set_then_find() {
294 let mut b = Bucket::EMPTY;
295 let a = Addr::new(Space::Arena, 0x1234_5678);
296 b.set(3, 0xAB, a);
297 let m = b.match_tag(0xAB);
298 assert_eq!(m.count(), 1);
299 assert_eq!(m.first(), Some(3));
300 assert_eq!(b.addr(3), a);
301 assert_eq!(b.tag(3), 0xAB);
302 }
303
304 #[test]
305 fn every_slot_round_trips_every_space() {
306 for &space in Space::ALL {
307 for i in 0..SLOTS {
308 let mut b = Bucket::EMPTY;
309 let a = Addr::new(space, yo_common::MAX_OFFSET);
310 b.set(i, 0x5A, a);
311 assert_eq!(b.addr(i), a, "slot {i} space {space:?}");
312 assert_eq!(b.match_tag(0x5A).first(), Some(i));
313 }
314 }
315 }
316
317 #[test]
321 fn the_last_slot_does_not_bleed_into_the_link() {
322 let mut b = Bucket::EMPTY;
323 let a = Addr::new(Space::Arena, 0xABCD);
324 b.set(SLOTS - 1, 0x11, a);
325 b.set_link(yo_common::MAX_OFFSET);
326 assert_eq!(b.addr(SLOTS - 1), a);
327 assert_eq!(b.link(), Some(yo_common::MAX_OFFSET));
328 }
329
330 #[test]
332 fn the_link_does_not_bleed_into_the_last_slot() {
333 let mut b = Bucket::EMPTY;
334 b.set_link(u64::MAX >> 8);
335 let a = Addr::new(Space::Graph, 7);
336 b.set(SLOTS - 1, 0x22, a);
337 assert_eq!(b.link(), Some(u64::MAX >> 8));
338 assert_eq!(b.addr(SLOTS - 1), a);
339 }
340
341 #[test]
342 fn all_seven_slots_are_independent() {
343 let mut b = Bucket::EMPTY;
344 for i in 0..SLOTS {
345 b.set(
346 i,
347 (i as u8) + 1,
348 Addr::new(Space::Arena, (i as u64 + 1) * 16),
349 );
350 }
351 assert!(b.is_full());
352 for i in 0..SLOTS {
353 assert_eq!(b.tag(i), (i as u8) + 1);
354 assert_eq!(b.addr(i).offset(), (i as u64 + 1) * 16);
355 assert_eq!(b.match_tag((i as u8) + 1).first(), Some(i));
356 }
357 }
358
359 #[test]
360 fn duplicate_tags_all_report() {
361 let mut b = Bucket::EMPTY;
362 b.set(1, 0x77, Addr::new(Space::Arena, 16));
363 b.set(4, 0x77, Addr::new(Space::Arena, 32));
364 b.set(6, 0x77, Addr::new(Space::Arena, 48));
365 let m = b.match_tag(0x77);
366 assert_eq!(m.count(), 3);
367 assert_eq!(m.collect::<Vec<_>>(), vec![1, 4, 6]);
368 }
369
370 #[test]
373 fn the_flags_byte_is_never_a_match() {
374 let mut b = Bucket::EMPTY;
375 b.set_link(1); assert!(
377 b.match_tag(1).is_empty(),
378 "flags leaked into the tag search"
379 );
380 assert_eq!(b.match_empty().count(), SLOTS as u32);
381 let c = Bucket::EMPTY;
384 assert_eq!(c.match_empty().count(), SLOTS as u32);
385 }
386
387 #[test]
388 fn clear_frees_the_slot() {
389 let mut b = Bucket::EMPTY;
390 b.set(2, 0x99, Addr::new(Space::Arena, 64));
391 assert_eq!(b.match_empty().count(), 6);
392 b.clear(2);
393 assert!(b.match_tag(0x99).is_empty());
394 assert_eq!(b.match_empty().count(), 7);
395 assert_eq!(b.addr(2), Addr::NONE);
396 }
397
398 #[test]
399 fn links_attach_and_detach() {
400 let mut b = Bucket::EMPTY;
401 assert_eq!(b.link(), None);
402 assert!(!b.has_overflow());
403 b.set_link(4096);
404 assert!(b.has_overflow());
405 assert_eq!(b.link(), Some(4096));
406 b.clear_link();
407 assert_eq!(b.link(), None);
408 assert!(!b.has_overflow());
409 }
410
411 #[test]
412 #[should_panic(expected = "empty tag")]
413 fn setting_the_empty_tag_is_refused() {
414 let mut b = Bucket::EMPTY;
415 b.set(0, EMPTY, Addr::new(Space::Arena, 16));
416 }
417
418 #[test]
422 fn swar_agrees_with_a_plain_scan() {
423 for pattern in 0u32..128 {
424 let mut b = Bucket::EMPTY;
425 for i in 0..SLOTS {
426 if pattern & (1 << i) != 0 {
427 b.set(i, (i as u8) + 1, Addr::new(Space::Arena, 16));
429 }
430 }
431 const MIRI_TAGS: [u8; 14] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0x40, 0x7f, 0x80, 0xff];
438 let tags: Vec<u8> = if cfg!(miri) {
439 MIRI_TAGS.to_vec()
440 } else {
441 (0..=255u8).collect()
442 };
443 for tag in tags {
444 let want: Vec<usize> = (0..SLOTS).filter(|&i| b.tags[i] == tag).collect();
445 let got: Vec<usize> = b.match_tag(tag).collect();
446 assert_eq!(got, want, "pattern {pattern:#b} tag {tag}");
447 }
448 }
449 }
450}