Skip to content

Commit a2547e3

Browse files
committed
Use a safe BucketIndex abstraction in VecCache
The current code for indexing into bucket arrays is quite tricky and unsafe, partly because it has to keep manually assuring the compiler that a bucket index is always less than 21. By encapsulating that knowledge in a 21-value enum, we can make the code clearer and safer, without giving up performance. Having a dedicated `BucketIndex` type could also help with further cleanups of `VecCache` indexing.
1 parent f8704be commit a2547e3

2 files changed

Lines changed: 220 additions & 44 deletions

File tree

compiler/rustc_data_structures/src/vec_cache.rs

Lines changed: 180 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
//!
77
//! This is currently used for query caching.
88
9-
use std::fmt::Debug;
9+
use std::fmt::{self, Debug};
1010
use std::marker::PhantomData;
11+
use std::ops::{Index, IndexMut};
1112
use std::sync::atomic::{AtomicPtr, AtomicU32, AtomicUsize, Ordering};
1213

1314
use rustc_index::Idx;
1415

16+
#[cfg(test)]
17+
mod tests;
18+
1519
struct Slot<V> {
1620
// We never construct &Slot<V> so it's fine for this to not be in an UnsafeCell.
1721
value: V,
@@ -28,7 +32,7 @@ struct Slot<V> {
2832
#[derive(Copy, Clone, Debug)]
2933
struct SlotIndex {
3034
// the index of the bucket in VecCache (0 to 20)
31-
bucket_idx: usize,
35+
bucket_idx: BucketIndex,
3236
// the index of the slot within the bucket
3337
index_in_bucket: usize,
3438
}
@@ -42,7 +46,7 @@ const ENTRIES_BY_BUCKET: [usize; BUCKETS] = {
4246
let mut key = 0;
4347
loop {
4448
let si = SlotIndex::from_index(key);
45-
entries[si.bucket_idx] = si.entries();
49+
entries[si.bucket_idx.to_usize()] = si.bucket_idx.capacity();
4650
if key == 0 {
4751
key = 1;
4852
} else if key == (1 << 31) {
@@ -57,11 +61,6 @@ const ENTRIES_BY_BUCKET: [usize; BUCKETS] = {
5761
const BUCKETS: usize = 21;
5862

5963
impl SlotIndex {
60-
/// The total possible number of entries in the bucket
61-
const fn entries(&self) -> usize {
62-
if self.bucket_idx == 0 { 1 << 12 } else { 1 << (self.bucket_idx + 11) }
63-
}
64-
6564
// This unpacks a flat u32 index into identifying which bucket it belongs to and the offset
6665
// within that bucket. As noted in the VecCache docs, buckets double in size with each index.
6766
// Typically that would mean 31 buckets (2^0 + 2^1 ... + 2^31 = u32::MAX - 1), but to reduce
@@ -73,32 +72,21 @@ impl SlotIndex {
7372
// slots (see `slot_index_exhaustive` in tests).
7473
#[inline]
7574
const fn from_index(idx: u32) -> Self {
76-
const FIRST_BUCKET_SHIFT: usize = 12;
77-
if idx < (1 << FIRST_BUCKET_SHIFT) {
78-
return SlotIndex { bucket_idx: 0, index_in_bucket: idx as usize };
79-
}
80-
// We already ruled out idx 0, so this `ilog2` never panics (and the check optimizes away)
81-
let bucket = idx.ilog2() as usize;
82-
let entries = 1 << bucket;
83-
SlotIndex {
84-
bucket_idx: bucket - FIRST_BUCKET_SHIFT + 1,
85-
index_in_bucket: idx as usize - entries,
86-
}
75+
let (bucket_idx, index_in_bucket) = BucketIndex::from_flat_index(idx as usize);
76+
SlotIndex { bucket_idx, index_in_bucket }
8777
}
8878

8979
// SAFETY: Buckets must be managed solely by functions here (i.e., get/put on SlotIndex) and
9080
// `self` comes from SlotIndex::from_index
9181
#[inline]
9282
unsafe fn get<V: Copy>(&self, buckets: &[AtomicPtr<Slot<V>>; 21]) -> Option<(V, u32)> {
93-
// SAFETY: `bucket_idx` is ilog2(u32).saturating_sub(11), which is at most 21, i.e.,
94-
// in-bounds of buckets. See `from_index` for computation.
95-
let bucket = unsafe { buckets.get_unchecked(self.bucket_idx) };
83+
let bucket = &buckets[self.bucket_idx];
9684
let ptr = bucket.load(Ordering::Acquire);
9785
// Bucket is not yet initialized: then we obviously won't find this entry in that bucket.
9886
if ptr.is_null() {
9987
return None;
10088
}
101-
debug_assert!(self.index_in_bucket < self.entries());
89+
debug_assert!(self.index_in_bucket < self.bucket_idx.capacity());
10290
// SAFETY: `bucket` was allocated (so <= isize in total bytes) to hold `entries`, so this
10391
// must be inbounds.
10492
let slot = unsafe { ptr.add(self.index_in_bucket) };
@@ -131,7 +119,7 @@ impl SlotIndex {
131119

132120
#[cold]
133121
#[inline(never)]
134-
fn initialize_bucket<V>(bucket: &AtomicPtr<Slot<V>>, bucket_idx: usize) -> *mut Slot<V> {
122+
fn initialize_bucket<V>(bucket: &AtomicPtr<Slot<V>>, bucket_idx: BucketIndex) -> *mut Slot<V> {
135123
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
136124

137125
// If we are initializing the bucket, then acquire a global lock.
@@ -145,8 +133,8 @@ impl SlotIndex {
145133
// OK, now under the allocator lock, if we're still null then it's definitely us that will
146134
// initialize this bucket.
147135
if ptr.is_null() {
148-
let bucket_len = SlotIndex { bucket_idx, index_in_bucket: 0 }.entries();
149-
let bucket_layout = std::alloc::Layout::array::<Slot<V>>(bucket_len).unwrap();
136+
let bucket_layout =
137+
std::alloc::Layout::array::<Slot<V>>(bucket_idx.capacity()).unwrap();
150138
// This is more of a sanity check -- this code is very cold, so it's safe to pay a
151139
// little extra cost here.
152140
assert!(bucket_layout.size() > 0);
@@ -167,12 +155,10 @@ impl SlotIndex {
167155
/// Returns true if this successfully put into the map.
168156
#[inline]
169157
fn put<V>(&self, buckets: &[AtomicPtr<Slot<V>>; 21], value: V, extra: u32) -> bool {
170-
// SAFETY: `bucket_idx` is ilog2(u32).saturating_sub(11), which is at most 21, i.e.,
171-
// in-bounds of buckets.
172-
let bucket = unsafe { buckets.get_unchecked(self.bucket_idx) };
158+
let bucket = &buckets[self.bucket_idx];
173159
let ptr = self.bucket_ptr(bucket);
174160

175-
debug_assert!(self.index_in_bucket < self.entries());
161+
debug_assert!(self.index_in_bucket < self.bucket_idx.capacity());
176162
// SAFETY: `bucket` was allocated (so <= isize in total bytes) to hold `entries`, so this
177163
// must be inbounds.
178164
let slot = unsafe { ptr.add(self.index_in_bucket) };
@@ -209,12 +195,10 @@ impl SlotIndex {
209195
/// Inserts into the map, given that the slot is unique, so it won't race with other threads.
210196
#[inline]
211197
unsafe fn put_unique<V>(&self, buckets: &[AtomicPtr<Slot<V>>; 21], value: V, extra: u32) {
212-
// SAFETY: `bucket_idx` is ilog2(u32).saturating_sub(11), which is at most 21, i.e.,
213-
// in-bounds of buckets.
214-
let bucket = unsafe { buckets.get_unchecked(self.bucket_idx) };
198+
let bucket = &buckets[self.bucket_idx];
215199
let ptr = self.bucket_ptr(bucket);
216200

217-
debug_assert!(self.index_in_bucket < self.entries());
201+
debug_assert!(self.index_in_bucket < self.bucket_idx.capacity());
218202
// SAFETY: `bucket` was allocated (so <= isize in total bytes) to hold `entries`, so this
219203
// must be inbounds.
220204
let slot = unsafe { ptr.add(self.index_in_bucket) };
@@ -289,7 +273,7 @@ unsafe impl<K: Idx, #[may_dangle] V, I> Drop for VecCache<K, V, I> {
289273
assert!(!std::mem::needs_drop::<K>());
290274
assert!(!std::mem::needs_drop::<V>());
291275

292-
for (idx, bucket) in self.buckets.iter().enumerate() {
276+
for (idx, bucket) in BucketIndex::enumerate_buckets(&self.buckets) {
293277
let bucket = bucket.load(Ordering::Acquire);
294278
if !bucket.is_null() {
295279
let layout = std::alloc::Layout::array::<Slot<V>>(ENTRIES_BY_BUCKET[idx]).unwrap();
@@ -299,7 +283,7 @@ unsafe impl<K: Idx, #[may_dangle] V, I> Drop for VecCache<K, V, I> {
299283
}
300284
}
301285

302-
for (idx, bucket) in self.present.iter().enumerate() {
286+
for (idx, bucket) in BucketIndex::enumerate_buckets(&self.present) {
303287
let bucket = bucket.load(Ordering::Acquire);
304288
if !bucket.is_null() {
305289
let layout = std::alloc::Layout::array::<Slot<()>>(ENTRIES_BY_BUCKET[idx]).unwrap();
@@ -365,5 +349,163 @@ where
365349
}
366350
}
367351

368-
#[cfg(test)]
369-
mod tests;
352+
/// Index into an array of buckets.
353+
///
354+
/// Using an enum lets us tell the compiler that values range from 0 to 20,
355+
/// without having to resort to pattern types or other unstable features.
356+
#[derive(Clone, Copy, PartialEq, Eq)]
357+
#[repr(usize)]
358+
enum BucketIndex {
359+
// tidy-alphabetical-start
360+
Bucket00,
361+
Bucket01,
362+
Bucket02,
363+
Bucket03,
364+
Bucket04,
365+
Bucket05,
366+
Bucket06,
367+
Bucket07,
368+
Bucket08,
369+
Bucket09,
370+
Bucket10,
371+
Bucket11,
372+
Bucket12,
373+
Bucket13,
374+
Bucket14,
375+
Bucket15,
376+
Bucket16,
377+
Bucket17,
378+
Bucket18,
379+
Bucket19,
380+
Bucket20,
381+
// tidy-alphabetical-end
382+
}
383+
384+
impl Debug for BucketIndex {
385+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386+
Debug::fmt(&self.to_usize(), f)
387+
}
388+
}
389+
390+
impl BucketIndex {
391+
/// Capacity of bucket 0 (and also of bucket 1).
392+
const BUCKET_0_CAPACITY: usize = 1 << (Self::NONZERO_BUCKET_SHIFT_ADJUST + 1);
393+
/// Adjustment factor from the highest-set-bit-position of a flat index,
394+
/// to its corresponding bucket number.
395+
///
396+
/// For example, the first flat-index in bucket 2 is 8192.
397+
/// Its highest-set-bit-position is `(8192).ilog2() == 13`, and subtracting
398+
/// the adjustment factor of 11 gives the bucket number of 2.
399+
const NONZERO_BUCKET_SHIFT_ADJUST: usize = 11;
400+
401+
#[inline(always)]
402+
const fn to_usize(self) -> usize {
403+
self as usize
404+
}
405+
406+
#[inline(always)]
407+
const fn from_raw(raw: usize) -> Self {
408+
match raw {
409+
// tidy-alphabetical-start
410+
00 => Self::Bucket00,
411+
01 => Self::Bucket01,
412+
02 => Self::Bucket02,
413+
03 => Self::Bucket03,
414+
04 => Self::Bucket04,
415+
05 => Self::Bucket05,
416+
06 => Self::Bucket06,
417+
07 => Self::Bucket07,
418+
08 => Self::Bucket08,
419+
09 => Self::Bucket09,
420+
10 => Self::Bucket10,
421+
11 => Self::Bucket11,
422+
12 => Self::Bucket12,
423+
13 => Self::Bucket13,
424+
14 => Self::Bucket14,
425+
15 => Self::Bucket15,
426+
16 => Self::Bucket16,
427+
17 => Self::Bucket17,
428+
18 => Self::Bucket18,
429+
19 => Self::Bucket19,
430+
20 => Self::Bucket20,
431+
// tidy-alphabetical-end
432+
_ => panic!("bucket index out of range"),
433+
}
434+
}
435+
436+
/// Total number of slots in this bucket.
437+
#[inline(always)]
438+
const fn capacity(self) -> usize {
439+
match self {
440+
Self::Bucket00 => Self::BUCKET_0_CAPACITY,
441+
// Bucket 1 has a capacity of `1 << (1 + 11) == pow(2, 12) == 4096`.
442+
// Bucket 2 has a capacity of `1 << (2 + 11) == pow(2, 13) == 8192`.
443+
_ => 1 << (self.to_usize() + Self::NONZERO_BUCKET_SHIFT_ADJUST),
444+
}
445+
}
446+
447+
/// Converts a flat index in the range `0..=u32::MAX` into a bucket index,
448+
/// and a slot offset within that bucket.
449+
///
450+
/// Panics if `flat > u32::MAX`.
451+
#[inline(always)]
452+
const fn from_flat_index(flat: usize) -> (Self, usize) {
453+
if flat > u32::MAX as usize {
454+
panic!();
455+
}
456+
457+
// If the index is in bucket 0, the conversion is trivial.
458+
// This also avoids calling `ilog2` when `flat == 0`.
459+
if flat < Self::BUCKET_0_CAPACITY {
460+
return (Self::Bucket00, flat);
461+
}
462+
463+
// General-case conversion for a non-zero bucket index.
464+
//
465+
// | bucket | slot
466+
// flat | ilog2 | index | offset
467+
// ------------------------------
468+
// 4096 | 12 | 1 | 0
469+
// 4097 | 12 | 1 | 1
470+
// ...
471+
// 8191 | 12 | 1 | 4095
472+
// 8192 | 13 | 2 | 0
473+
let highest_bit_pos = flat.ilog2() as usize;
474+
let bucket_index =
475+
BucketIndex::from_raw(highest_bit_pos - Self::NONZERO_BUCKET_SHIFT_ADJUST);
476+
477+
// Clear the highest-set bit (which selects the bucket) to get the
478+
// slot offset within this bucket.
479+
let slot_offset = flat & !(1 << highest_bit_pos);
480+
481+
(bucket_index, slot_offset)
482+
}
483+
484+
#[inline(always)]
485+
fn iter_all() -> impl ExactSizeIterator<Item = Self> {
486+
(0usize..BUCKETS).map(BucketIndex::from_raw)
487+
}
488+
489+
#[inline(always)]
490+
fn enumerate_buckets<T>(buckets: &[T; BUCKETS]) -> impl ExactSizeIterator<Item = (Self, &T)> {
491+
BucketIndex::iter_all().zip(buckets)
492+
}
493+
}
494+
495+
impl<T> Index<BucketIndex> for [T; BUCKETS] {
496+
type Output = T;
497+
498+
#[inline(always)]
499+
fn index(&self, index: BucketIndex) -> &Self::Output {
500+
// The optimizer should be able to see that see that a bucket index is
501+
// always in-bounds, and omit the runtime bounds check.
502+
&self[index.to_usize()]
503+
}
504+
}
505+
506+
impl<T> IndexMut<BucketIndex> for [T; BUCKETS] {
507+
#[inline(always)]
508+
fn index_mut(&mut self, index: BucketIndex) -> &mut Self::Output {
509+
&mut self[index.to_usize()]
510+
}
511+
}

compiler/rustc_data_structures/src/vec_cache/tests.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,39 @@
11
use super::*;
22

3+
#[test]
4+
#[should_panic(expected = "bucket index out of range")]
5+
fn bucket_index_n_buckets() {
6+
BucketIndex::from_raw(BUCKETS);
7+
}
8+
9+
#[test]
10+
fn bucket_index_round_trip() {
11+
for i in 0..BUCKETS {
12+
assert_eq!(BucketIndex::from_raw(i).to_usize(), i);
13+
}
14+
}
15+
16+
#[test]
17+
fn bucket_index_iter_all_len() {
18+
let len = BucketIndex::iter_all().len();
19+
assert_eq!(len, BUCKETS);
20+
21+
let len = BucketIndex::iter_all().collect::<Vec<_>>().len();
22+
assert_eq!(len, BUCKETS);
23+
24+
let len = BucketIndex::enumerate_buckets(&[(); BUCKETS]).len();
25+
assert_eq!(len, BUCKETS);
26+
}
27+
28+
#[test]
29+
fn bucket_index_capacity() {
30+
let mut total = 0u128;
31+
for i in BucketIndex::iter_all() {
32+
total += u128::try_from(i.capacity()).unwrap();
33+
}
34+
assert_eq!(total, (u32::MAX as u128) + 1);
35+
}
36+
337
#[test]
438
#[cfg(not(miri))]
539
fn vec_cache_empty() {
@@ -70,8 +104,8 @@ fn slot_entries_table() {
70104

71105
#[test]
72106
fn bucket_entries_matches() {
73-
for i in 0..BUCKETS {
74-
assert_eq!(SlotIndex { bucket_idx: i, index_in_bucket: 0 }.entries(), ENTRIES_BY_BUCKET[i]);
107+
for i in BucketIndex::iter_all() {
108+
assert_eq!(i.capacity(), ENTRIES_BY_BUCKET[i]);
75109
}
76110
}
77111

@@ -84,22 +118,22 @@ fn slot_index_exhaustive() {
84118
}
85119
let slot_idx = SlotIndex::from_index(0);
86120
assert_eq!(slot_idx.index_in_bucket, 0);
87-
assert_eq!(slot_idx.bucket_idx, 0);
121+
assert_eq!(slot_idx.bucket_idx, BucketIndex::Bucket00);
88122
let mut prev = slot_idx;
89123
for idx in 1..=u32::MAX {
90124
let slot_idx = SlotIndex::from_index(idx);
91125

92126
// SAFETY: Ensure indices don't go out of bounds of buckets.
93-
assert!(slot_idx.index_in_bucket < slot_idx.entries());
127+
assert!(slot_idx.index_in_bucket < slot_idx.bucket_idx.capacity());
94128

95129
if prev.bucket_idx == slot_idx.bucket_idx {
96130
assert_eq!(prev.index_in_bucket + 1, slot_idx.index_in_bucket);
97131
} else {
98132
assert_eq!(slot_idx.index_in_bucket, 0);
99133
}
100134

101-
assert_eq!(buckets[slot_idx.bucket_idx], slot_idx.entries() as u32);
102-
assert_eq!(ENTRIES_BY_BUCKET[slot_idx.bucket_idx], slot_idx.entries(), "{}", idx);
135+
assert_eq!(buckets[slot_idx.bucket_idx], slot_idx.bucket_idx.capacity() as u32);
136+
assert_eq!(ENTRIES_BY_BUCKET[slot_idx.bucket_idx], slot_idx.bucket_idx.capacity(), "{idx}",);
103137

104138
prev = slot_idx;
105139
}

0 commit comments

Comments
 (0)