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