-
Notifications
You must be signed in to change notification settings - Fork 390
Implement a garbage collector for tags #2479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,6 +80,8 @@ pub struct Stacks { | |
history: AllocHistory, | ||
/// The set of tags that have been exposed inside this allocation. | ||
exposed_tags: FxHashSet<SbTag>, | ||
/// Whether this memory has been modified since the last time the tag GC ran | ||
modified_since_last_gc: bool, | ||
} | ||
|
||
/// Extra global state, available to the memory access hooks. | ||
|
@@ -422,6 +424,7 @@ impl<'tcx> Stack { | |
let item = self.get(idx).unwrap(); | ||
Stack::item_popped(&item, global, dcx)?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
|
@@ -496,6 +499,20 @@ impl<'tcx> Stack { | |
} | ||
// # Stacked Borrows Core End | ||
|
||
/// Integration with the SbTag garbage collector | ||
impl Stacks { | ||
pub fn remove_unreachable_tags(&mut self, live_tags: &FxHashSet<SbTag>) { | ||
if self.modified_since_last_gc { | ||
for stack in self.stacks.iter_mut_all() { | ||
if stack.len() > 64 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why 64? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haha this should definitely be explained in a comment. This is a magic number guesstimated by benchmarking, like the stack-cache length. Skipping over small stacks is a very crude generational garbage collector. I'll definitely make a PR that addresses this later in the day. |
||
stack.retain(live_tags); | ||
} | ||
} | ||
self.modified_since_last_gc = false; | ||
} | ||
} | ||
} | ||
|
||
/// Map per-stack operations to higher-level per-location-range operations. | ||
impl<'tcx> Stacks { | ||
/// Creates a new stack with an initial tag. For diagnostic purposes, we also need to know | ||
|
@@ -514,6 +531,7 @@ impl<'tcx> Stacks { | |
stacks: RangeMap::new(size, stack), | ||
history: AllocHistory::new(id, item, current_span), | ||
exposed_tags: FxHashSet::default(), | ||
modified_since_last_gc: false, | ||
} | ||
} | ||
|
||
|
@@ -528,6 +546,7 @@ impl<'tcx> Stacks { | |
&mut FxHashSet<SbTag>, | ||
) -> InterpResult<'tcx>, | ||
) -> InterpResult<'tcx> { | ||
self.modified_since_last_gc = true; | ||
for (offset, stack) in self.stacks.iter_mut(range.start, range.size) { | ||
let mut dcx = dcx_builder.build(&mut self.history, offset); | ||
f(stack, &mut dcx, &mut self.exposed_tags)?; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,61 @@ pub struct Stack { | |
unique_range: Range<usize>, | ||
} | ||
|
||
impl Stack { | ||
pub fn retain(&mut self, tags: &FxHashSet<SbTag>) { | ||
let mut first_removed = None; | ||
|
||
let mut read_idx = 1; | ||
let mut write_idx = 1; | ||
while read_idx < self.borrows.len() { | ||
let left = self.borrows[read_idx - 1]; | ||
let this = self.borrows[read_idx]; | ||
let should_keep = match this.perm() { | ||
// SharedReadWrite is the simplest case, if it's unreachable we can just remove it. | ||
Permission::SharedReadWrite => tags.contains(&this.tag()), | ||
// Only retain a Disabled tag if it is terminating a SharedReadWrite block. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is special, we delete them even if they are live! (That's correct of course but I had to read this twice to realize that this |
||
Permission::Disabled => left.perm() == Permission::SharedReadWrite, | ||
// Unique and SharedReadOnly can terminate a SharedReadWrite block, so only remove | ||
// them if they are both unreachable and not directly after a SharedReadWrite. | ||
Permission::Unique | Permission::SharedReadOnly => | ||
left.perm() == Permission::SharedReadWrite || tags.contains(&this.tag()), | ||
}; | ||
|
||
if should_keep { | ||
if read_idx != write_idx { | ||
self.borrows[write_idx] = self.borrows[read_idx]; | ||
} | ||
write_idx += 1; | ||
} else if first_removed.is_none() { | ||
first_removed = Some(read_idx); | ||
} | ||
|
||
read_idx += 1; | ||
} | ||
self.borrows.truncate(write_idx); | ||
|
||
#[cfg(not(feature = "stack-cache"))] | ||
drop(first_removed); // This is only needed for the stack-cache | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fighting with the fact that this is unused if the stack-cache is off here. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should mention that we don't have to repair the stack cache. Just tossing the whole thing is a valid approach (it warms up quickly and GC runs are intentionally rare), but I feel like justifying it is awkward. But maybe this issue tips the scales? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah I see. I would use |
||
|
||
#[cfg(feature = "stack-cache")] | ||
if let Some(first_removed) = first_removed { | ||
// Either end of unique_range may have shifted, all we really know is that we can't | ||
// have introduced a new Unique. | ||
if !self.unique_range.is_empty() { | ||
self.unique_range = 0..self.len(); | ||
} | ||
|
||
// Replace any Items which have been collected with the base item, a known-good value. | ||
for i in 0..CACHE_LEN { | ||
if self.cache.idx[i] >= first_removed { | ||
self.cache.items[i] = self.borrows[0]; | ||
self.cache.idx[i] = 0; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// A very small cache of searches of a borrow stack, mapping `Item`s to their position in said stack. | ||
/// | ||
/// It may seem like maintaining this cache is a waste for small stacks, but | ||
|
@@ -105,14 +160,11 @@ impl<'tcx> Stack { | |
|
||
// Check that the unique_range is a valid index into the borrow stack. | ||
// This asserts that the unique_range's start <= end. | ||
let uniques = &self.borrows[self.unique_range.clone()]; | ||
let _uniques = &self.borrows[self.unique_range.clone()]; | ||
|
||
// Check that the start of the unique_range is precise. | ||
if let Some(first_unique) = uniques.first() { | ||
assert_eq!(first_unique.perm(), Permission::Unique); | ||
} | ||
// We cannot assert that the unique range is exact on the upper end. | ||
// When we pop items within the unique range, setting the end of the range precisely | ||
// We cannot assert that the unique range is precise. | ||
// Both ends may shift around when `Stack::retain` is called. Additionally, | ||
// when we pop items within the unique range, setting the end of the range precisely | ||
// requires doing a linear search of the borrow stack, which is exactly the kind of | ||
// operation that all this caching exists to avoid. | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
use crate::*; | ||
use rustc_data_structures::fx::FxHashSet; | ||
|
||
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriEvalContext<'mir, 'tcx> {} | ||
pub trait EvalContextExt<'mir, 'tcx: 'mir>: MiriEvalContextExt<'mir, 'tcx> { | ||
fn garbage_collect_tags(&mut self) -> InterpResult<'tcx> { | ||
let this = self.eval_context_mut(); | ||
// No reason to do anything at all if stacked borrows is off. | ||
if this.machine.stacked_borrows.is_none() { | ||
return Ok(()); | ||
} | ||
|
||
let mut tags = FxHashSet::default(); | ||
|
||
for thread in this.machine.threads.iter() { | ||
if let Some(Scalar::Ptr( | ||
Pointer { provenance: Provenance::Concrete { sb, .. }, .. }, | ||
_, | ||
)) = thread.panic_payload | ||
{ | ||
tags.insert(sb); | ||
} | ||
} | ||
|
||
self.find_tags_in_tls(&mut tags); | ||
self.find_tags_in_memory(&mut tags); | ||
self.find_tags_in_locals(&mut tags)?; | ||
Comment on lines
+15
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be factored to use a more general "visit all the values that exist in the machine" kind of operation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds like a good idea but I don't know how to implement it. |
||
|
||
self.remove_unreachable_tags(tags); | ||
|
||
Ok(()) | ||
} | ||
|
||
fn find_tags_in_tls(&mut self, tags: &mut FxHashSet<SbTag>) { | ||
let this = self.eval_context_mut(); | ||
this.machine.tls.iter(|scalar| { | ||
if let Scalar::Ptr(Pointer { provenance: Provenance::Concrete { sb, .. }, .. }, _) = | ||
scalar | ||
{ | ||
tags.insert(*sb); | ||
} | ||
}); | ||
} | ||
|
||
fn find_tags_in_memory(&mut self, tags: &mut FxHashSet<SbTag>) { | ||
let this = self.eval_context_mut(); | ||
this.memory.alloc_map().iter(|it| { | ||
for (_id, (_kind, alloc)) in it { | ||
for (_size, prov) in alloc.provenance().iter() { | ||
if let Provenance::Concrete { sb, .. } = prov { | ||
tags.insert(*sb); | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
|
||
fn find_tags_in_locals(&mut self, tags: &mut FxHashSet<SbTag>) -> InterpResult<'tcx> { | ||
let this = self.eval_context_mut(); | ||
for frame in this.machine.threads.all_stacks().flatten() { | ||
// Handle the return place of each frame | ||
if let Ok(return_place) = frame.return_place.try_as_mplace() { | ||
if let Some(Provenance::Concrete { sb, .. }) = return_place.ptr.provenance { | ||
tags.insert(sb); | ||
} | ||
} | ||
|
||
for local in frame.locals.iter() { | ||
let LocalValue::Live(value) = local.value else { | ||
continue; | ||
}; | ||
match value { | ||
Operand::Immediate(Immediate::Scalar(Scalar::Ptr(ptr, _))) => | ||
if let Provenance::Concrete { sb, .. } = ptr.provenance { | ||
tags.insert(sb); | ||
}, | ||
Operand::Immediate(Immediate::ScalarPair(s1, s2)) => { | ||
if let Scalar::Ptr(ptr, _) = s1 { | ||
if let Provenance::Concrete { sb, .. } = ptr.provenance { | ||
tags.insert(sb); | ||
} | ||
} | ||
if let Scalar::Ptr(ptr, _) = s2 { | ||
if let Provenance::Concrete { sb, .. } = ptr.provenance { | ||
tags.insert(sb); | ||
} | ||
} | ||
} | ||
Operand::Indirect(MemPlace { ptr, .. }) => { | ||
if let Some(Provenance::Concrete { sb, .. }) = ptr.provenance { | ||
tags.insert(sb); | ||
} | ||
} | ||
Operand::Immediate(Immediate::Uninit) | ||
| Operand::Immediate(Immediate::Scalar(Scalar::Int(_))) => {} | ||
} | ||
} | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
fn remove_unreachable_tags(&mut self, tags: FxHashSet<SbTag>) { | ||
let this = self.eval_context_mut(); | ||
this.memory.alloc_map().iter(|it| { | ||
for (_id, (_kind, alloc)) in it { | ||
alloc | ||
.extra | ||
.stacked_borrows | ||
.as_ref() | ||
.unwrap() | ||
.borrow_mut() | ||
.remove_unreachable_tags(&tags); | ||
} | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the comment refers to linux, but the condition to macOS, slightly confusing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦 perfectly fine content for a second PR