]> git.lizzy.rs Git - rust.git/blob - src/stacked_borrows/stack.rs
Rearrange and document the new implementation
[rust.git] / src / stacked_borrows / stack.rs
1 use crate::stacked_borrows::{AccessKind, Item, Permission, SbTag, SbTagExtra};
2 use rustc_data_structures::fx::FxHashSet;
3 #[cfg(feature = "stack-cache")]
4 use std::ops::Range;
5
6 /// Exactly what cache size we should use is a difficult tradeoff. There will always be some
7 /// workload which has a `SbTag` working set which exceeds the size of the cache, and ends up
8 /// falling back to linear searches of the borrow stack very often.
9 /// The cost of making this value too large is that the loop in `Stack::insert` which ensures the
10 /// entries in the cache stay correct after an insert becomes expensive.
11 #[cfg(feature = "stack-cache")]
12 const CACHE_LEN: usize = 32;
13
14 /// Extra per-location state.
15 #[derive(Clone, Debug)]
16 pub struct Stack {
17     /// Used *mostly* as a stack; never empty.
18     /// Invariants:
19     /// * Above a `SharedReadOnly` there can only be more `SharedReadOnly`.
20     /// * Except for `Untagged`, no tag occurs in the stack more than once.
21     borrows: Vec<Item>,
22     /// If this is `Some(id)`, then the actual current stack is unknown. This can happen when
23     /// wildcard pointers are used to access this location. What we do know is that `borrows` are at
24     /// the top of the stack, and below it are arbitrarily many items whose `tag` is strictly less
25     /// than `id`.
26     /// When the bottom is unknown, `borrows` always has a `SharedReadOnly` or `Unique` at the bottom;
27     /// we never have the unknown-to-known boundary in an SRW group.
28     unknown_bottom: Option<SbTag>,
29
30     /// A small LRU cache of searches of the borrow stack.
31     #[cfg(feature = "stack-cache")]
32     cache: StackCache,
33     /// On a read, we need to disable all `Unique` above the granting item. We can avoid most of
34     /// this scan by keeping track of the region of the borrow stack that may contain `Unique`s.
35     #[cfg(feature = "stack-cache")]
36     unique_range: Range<usize>,
37 }
38
39 /// A very small cache of searches of the borrow stack
40 /// This maps items to locations in the borrow stack. Any use of this still needs to do a
41 /// probably-cold random access into the borrow stack to figure out what `Permission` an
42 /// `SbTag` grants. We could avoid this by also storing the `Permission` in the cache, but
43 /// most lookups into the cache are immediately followed by access of the full borrow stack anyway.
44 ///
45 /// It may seem like maintaining this cache is a waste for small stacks, but
46 /// (a) iterating over small fixed-size arrays is super fast, and (b) empirically this helps *a lot*,
47 /// probably because runtime is dominated by large stacks.
48 #[cfg(feature = "stack-cache")]
49 #[derive(Clone, Debug)]
50 struct StackCache {
51     items: [Item; CACHE_LEN], // Hot in find_granting
52     idx: [usize; CACHE_LEN],  // Hot in grant
53 }
54
55 #[cfg(feature = "stack-cache")]
56 impl StackCache {
57     /// When a tag is used, we call this function to add or refresh it in the cache.
58     ///
59     /// We use the position in the cache to represent how recently a tag was used; the first position
60     /// is the most recently used tag. So an add shifts every element towards the end, and inserts
61     /// the new element at the start. We lose the last element.
62     /// This strategy is effective at keeping the most-accessed items in the cache, but it costs a
63     /// linear shift across the entire cache when we add a new tag.
64     fn add(&mut self, idx: usize, item: Item) {
65         self.items.copy_within(0..CACHE_LEN - 1, 1);
66         self.items[0] = item;
67         self.idx.copy_within(0..CACHE_LEN - 1, 1);
68         self.idx[0] = idx;
69     }
70 }
71
72 impl PartialEq for Stack {
73     fn eq(&self, other: &Self) -> bool {
74         // All the semantics of Stack are in self.borrows, everything else is caching
75         self.borrows == other.borrows
76     }
77 }
78
79 impl Eq for Stack {}
80
81 impl<'tcx> Stack {
82     /// Panics if any of the caching mechanisms have broken,
83     /// - The StackCache indices don't refer to the parallel items,
84     /// - There are no Unique items outside of first_unique..last_unique
85     #[cfg(feature = "expensive-debug-assertions")]
86     fn verify_cache_consistency(&self) {
87         // Only a full cache needs to be valid. Also see the comments in find_granting_cache
88         // and set_unknown_bottom.
89         if self.borrows.len() >= CACHE_LEN {
90             for (tag, stack_idx) in self.cache.items.iter().zip(self.cache.idx.iter()) {
91                 assert_eq!(self.borrows[*stack_idx], *tag);
92             }
93         }
94
95         for (idx, item) in self.borrows.iter().enumerate() {
96             if item.perm() == Permission::Unique {
97                 assert!(
98                     self.unique_range.contains(&idx),
99                     "{:?} {:?}",
100                     self.unique_range,
101                     self.borrows
102                 );
103             }
104         }
105     }
106
107     /// Find the item granting the given kind of access to the given tag, and return where
108     /// it is on the stack. For wildcard tags, the given index is approximate, but if *no*
109     /// index is given it means the match was *not* in the known part of the stack.
110     /// `Ok(None)` indicates it matched the "unknown" part of the stack.
111     /// `Err` indicates it was not found.
112     pub(super) fn find_granting(
113         &mut self,
114         access: AccessKind,
115         tag: SbTagExtra,
116         exposed_tags: &FxHashSet<SbTag>,
117     ) -> Result<Option<usize>, ()> {
118         #[cfg(feature = "expensive-debug-assertions")]
119         self.verify_cache_consistency();
120
121         let SbTagExtra::Concrete(tag) = tag else {
122             // Handle the wildcard case.
123             // Go search the stack for an exposed tag.
124             if let Some(idx) =
125                 self.borrows
126                     .iter()
127                     .enumerate() // we also need to know *where* in the stack
128                     .rev() // search top-to-bottom
129                     .find_map(|(idx, item)| {
130                         // If the item fits and *might* be this wildcard, use it.
131                         if item.perm().grants(access) && exposed_tags.contains(&item.tag()) {
132                             Some(idx)
133                         } else {
134                             None
135                         }
136                     })
137             {
138                 return Ok(Some(idx));
139             }
140             // If we couldn't find it in the stack, check the unknown bottom.
141             return if self.unknown_bottom.is_some() { Ok(None) } else { Err(()) };
142         };
143
144         if let Some(idx) = self.find_granting_tagged(access, tag) {
145             return Ok(Some(idx));
146         }
147
148         // Couldn't find it in the stack; but if there is an unknown bottom it might be there.
149         let found = self.unknown_bottom.is_some_and(|&unknown_limit| {
150             tag.0 < unknown_limit.0 // unknown_limit is an upper bound for what can be in the unknown bottom.
151         });
152         if found { Ok(None) } else { Err(()) }
153     }
154
155     fn find_granting_tagged(&mut self, access: AccessKind, tag: SbTag) -> Option<usize> {
156         #[cfg(feature = "stack-cache")]
157         if let Some(idx) = self.find_granting_cache(access, tag) {
158             return Some(idx);
159         }
160
161         // If we didn't find the tag in the cache, fall back to a linear search of the
162         // whole stack, and add the tag to the cache.
163         for (stack_idx, item) in self.borrows.iter().enumerate().rev() {
164             if tag == item.tag() && item.perm().grants(access) {
165                 #[cfg(feature = "stack-cache")]
166                 self.cache.add(stack_idx, *item);
167                 return Some(stack_idx);
168             }
169         }
170         None
171     }
172
173     #[cfg(feature = "stack-cache")]
174     fn find_granting_cache(&mut self, access: AccessKind, tag: SbTag) -> Option<usize> {
175         // This looks like a common-sense optimization; we're going to do a linear search of the
176         // cache or the borrow stack to scan the shorter of the two. This optimization is miniscule
177         // and this check actually ensures we do not access an invalid cache.
178         // When a stack is created and when items are removed from the top of the borrow stack, we
179         // need some valid value to populate the cache. In both cases, we try to use the bottom
180         // item. But when the stack is cleared in `set_unknown_bottom` there is nothing we could
181         // place in the cache that is correct. But due to the way we populate the cache in
182         // `StackCache::add`, we know that when the borrow stack has grown larger than the cache,
183         // every slot in the cache is valid.
184         if self.borrows.len() <= CACHE_LEN {
185             return None;
186         }
187         // Search the cache for the tag we're looking up
188         let cache_idx = self.cache.items.iter().position(|t| t.tag() == tag)?;
189         let stack_idx = self.cache.idx[cache_idx];
190         // If we found the tag, look up its position in the stack to see if it grants
191         // the required permission
192         if self.cache.items[cache_idx].perm().grants(access) {
193             // If it does, and it's not already in the most-recently-used position, re-insert it at
194             // the most-recently-used position. This technically reduces the efficiency of the
195             // cache by duplicating elements, but current benchmarks do not seem to benefit from
196             // avoiding this duplication.
197             // But if the tag is in position 1, avoiding the duplicating add is trivial.
198             // If it does, and it's not already in the most-recently-used position, move it there.
199             // Except if the tag is in position 1, this is equivalent to just a swap, so do that.
200             if cache_idx == 1 {
201                 self.cache.items.swap(0, 1);
202                 self.cache.idx.swap(0, 1);
203             } else if cache_idx > 1 {
204                 self.cache.add(stack_idx, self.cache.items[cache_idx]);
205             }
206             Some(stack_idx)
207         } else {
208             // Tag is in the cache, but it doesn't grant the required permission
209             None
210         }
211     }
212
213     pub fn insert(&mut self, new_idx: usize, new: Item) {
214         self.borrows.insert(new_idx, new);
215
216         #[cfg(feature = "stack-cache")]
217         self.insert_cache(new_idx, new);
218     }
219
220     #[cfg(feature = "stack-cache")]
221     fn insert_cache(&mut self, new_idx: usize, new: Item) {
222         // Adjust the possibly-unique range if an insert occurs before or within it
223         if self.unique_range.start >= new_idx {
224             self.unique_range.start += 1;
225         }
226         if self.unique_range.end >= new_idx {
227             self.unique_range.end += 1;
228         }
229         if new.perm() == Permission::Unique {
230             // Make sure the possibly-unique range contains the new borrow
231             self.unique_range.start = self.unique_range.start.min(new_idx);
232             self.unique_range.end = self.unique_range.end.max(new_idx + 1);
233         }
234
235         // The above insert changes the meaning of every index in the cache >= new_idx, so now
236         // we need to find every one of those indexes and increment it.
237         // But if the insert is at the end (equivalent to a push), we can skip this step because
238         // it didn't change the position of any other items.
239         if new_idx != self.borrows.len() - 1 {
240             for idx in &mut self.cache.idx {
241                 if *idx >= new_idx {
242                     *idx += 1;
243                 }
244             }
245         }
246
247         // This primes the cache for the next access, which is almost always the just-added tag.
248         self.cache.add(new_idx, new);
249
250         #[cfg(feature = "expensive-debug-assertions")]
251         self.verify_cache_consistency();
252     }
253
254     /// Construct a new `Stack` using the passed `Item` as the base tag.
255     pub fn new(item: Item) -> Self {
256         Stack {
257             borrows: vec![item],
258             unknown_bottom: None,
259             #[cfg(feature = "stack-cache")]
260             cache: StackCache { idx: [0; CACHE_LEN], items: [item; CACHE_LEN] },
261             #[cfg(feature = "stack-cache")]
262             unique_range: if item.perm() == Permission::Unique { 0..1 } else { 0..0 },
263         }
264     }
265
266     pub fn get(&self, idx: usize) -> Option<Item> {
267         self.borrows.get(idx).cloned()
268     }
269
270     #[allow(clippy::len_without_is_empty)] // Stacks are never empty
271     pub fn len(&self) -> usize {
272         self.borrows.len()
273     }
274
275     pub fn unknown_bottom(&self) -> Option<SbTag> {
276         self.unknown_bottom
277     }
278
279     pub fn set_unknown_bottom(&mut self, tag: SbTag) {
280         // We clear the borrow stack but the lookup cache doesn't support clearing per se. Instead,
281         // there is a check explained in `find_granting_cache` which protects against accessing the
282         // cache when it has been cleared and not yet refilled.
283         self.borrows.clear();
284         self.unknown_bottom = Some(tag);
285     }
286
287     /// Find all `Unique` elements in this borrow stack above `granting_idx`, pass a copy of them
288     /// to the `visitor`, then set their `Permission` to `Disabled`.
289     pub fn disable_uniques_starting_at<V: FnMut(Item) -> crate::InterpResult<'tcx>>(
290         &mut self,
291         disable_start: usize,
292         mut visitor: V,
293     ) -> crate::InterpResult<'tcx> {
294         #[cfg(feature = "stack-cache")]
295         let unique_range = self.unique_range.clone();
296         #[cfg(not(feature = "stack-cache"))]
297         let unique_range = 0..self.len();
298
299         if disable_start <= unique_range.end {
300             let lower = unique_range.start.max(disable_start);
301             let upper = (unique_range.end + 1).min(self.borrows.len());
302             for item in &mut self.borrows[lower..upper] {
303                 if item.perm() == Permission::Unique {
304                     log::trace!("access: disabling item {:?}", item);
305                     visitor(*item)?;
306                     item.set_permission(Permission::Disabled);
307                     // Also update all copies of this item in the cache.
308                     for it in &mut self.cache.items {
309                         if it.tag() == item.tag() {
310                             it.set_permission(Permission::Disabled);
311                         }
312                     }
313                 }
314             }
315         }
316
317         #[cfg(feature = "stack-cache")]
318         if disable_start < self.unique_range.start {
319             // We disabled all Unique items
320             self.unique_range.start = 0;
321             self.unique_range.end = 0;
322         } else {
323             // Truncate the range to disable_start. This is + 2 because we are only removing
324             // elements after disable_start, and this range does not include the end.
325             self.unique_range.end = self.unique_range.end.min(disable_start + 1);
326         }
327
328         #[cfg(feature = "expensive-debug-assertions")]
329         self.verify_cache_consistency();
330
331         Ok(())
332     }
333
334     /// Produces an iterator which iterates over `range` in reverse, and when dropped removes that
335     /// range of `Item`s from this `Stack`.
336     pub fn pop_items_after<V: FnMut(Item) -> crate::InterpResult<'tcx>>(
337         &mut self,
338         start: usize,
339         mut visitor: V,
340     ) -> crate::InterpResult<'tcx> {
341         while self.borrows.len() > start {
342             let item = self.borrows.pop().unwrap();
343             visitor(item)?;
344         }
345
346         #[cfg(feature = "stack-cache")]
347         if !self.borrows.is_empty() {
348             // After we remove from the borrow stack, every aspect of our caching may be invalid, but it is
349             // also possible that the whole cache is still valid. So we call this method to repair what
350             // aspects of the cache are now invalid, instead of resetting the whole thing to a trivially
351             // valid default state.
352             let base_tag = self.borrows[0];
353             let mut removed = 0;
354             let mut cursor = 0;
355             // Remove invalid entries from the cache by rotating them to the end of the cache, then
356             // keep track of how many invalid elements there are and overwrite them with the base tag.
357             // The base tag here serves as a harmless default value.
358             for _ in 0..CACHE_LEN - 1 {
359                 if self.cache.idx[cursor] >= start {
360                     self.cache.idx[cursor..CACHE_LEN - removed].rotate_left(1);
361                     self.cache.items[cursor..CACHE_LEN - removed].rotate_left(1);
362                     removed += 1;
363                 } else {
364                     cursor += 1;
365                 }
366             }
367             for i in CACHE_LEN - removed - 1..CACHE_LEN {
368                 self.cache.idx[i] = 0;
369                 self.cache.items[i] = base_tag;
370             }
371
372             if start < self.unique_range.start.saturating_sub(1) {
373                 // We removed all the Unique items
374                 self.unique_range = 0..0;
375             } else {
376                 // Ensure the range doesn't extend past the new top of the stack
377                 self.unique_range.end = self.unique_range.end.min(start + 1);
378             }
379         } else {
380             self.unique_range = 0..0;
381         }
382
383         #[cfg(feature = "expensive-debug-assertions")]
384         self.verify_cache_consistency();
385         Ok(())
386     }
387 }