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 { retain(&mut self, tags: &FxHashSet<BorTag>)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 mitigates 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 let _unused = 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. add(&mut self, idx: usize, item: Item)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 { eq(&self, other: &Self) -> bool137 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))] verify_cache_consistency(&self)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. find_granting( &mut self, access: AccessKind, tag: ProvenanceExtra, exposed_tags: &FxHashSet<BorTag>, ) -> Result<Option<usize>, ()>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 find_granting_tagged(&mut self, access: AccessKind, tag: BorTag) -> Option<usize>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")] find_granting_cache(&mut self, access: AccessKind, tag: BorTag) -> Option<usize>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 insert(&mut self, new_idx: usize, new: Item)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")] insert_cache(&mut self, new_idx: usize, new: Item)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. new(item: Item) -> Self335 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 get(&self, idx: usize) -> Option<Item>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 len(&self) -> usize351 pub fn len(&self) -> usize { 352 self.borrows.len() 353 } 354 unknown_bottom(&self) -> Option<BorTag>355 pub fn unknown_bottom(&self) -> Option<BorTag> { 356 self.unknown_bottom 357 } 358 set_unknown_bottom(&mut self, tag: BorTag)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`. disable_uniques_starting_at( &mut self, disable_start: usize, mut visitor: impl FnMut(Item) -> crate::InterpResult<'tcx>, ) -> crate::InterpResult<'tcx>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`. pop_items_after<V: FnMut(Item) -> crate::InterpResult<'tcx>>( &mut self, start: usize, mut visitor: V, ) -> crate::InterpResult<'tcx>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 } 473