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