Skip to content
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

Implement Edge::update_for_forwarding #1037

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions docs/userguide/src/tutorial/code/mygc_semispace/gc_work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ impl<VM: VMBinding> ProcessEdgesWork for MyGCProcessEdges<VM> {
}

fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
if object.is_null() {
return object;
}
debug_assert!(!object.is_null());
let worker = self.worker();
let queue = &mut self.base.nodes;
if self.plan.tospace().in_space(object) {
Expand Down
4 changes: 1 addition & 3 deletions docs/userguide/src/tutorial/mygc/ss/exercise_solution.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,7 @@ In `gc_work.rs`:
the tospace and fromspace:
```rust
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
if object.is_null() {
return object;
}
debug_assert!(!object.is_null());

// Add this!
else if self.plan().youngspace().in_space(object) {
Expand Down
16 changes: 9 additions & 7 deletions src/plan/generational/gc_work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,21 @@ impl<VM: VMBinding, P: GenerationalPlanExt<VM> + PlanTraceObject<VM>> ProcessEdg
Self { plan, base }
}
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
if object.is_null() {
return object;
}
debug_assert!(!object.is_null());

// We cannot borrow `self` twice in a call, so we extract `worker` as a local variable.
let worker = self.worker();
self.plan
.trace_object_nursery(&mut self.base.nodes, object, worker)
}
fn process_edge(&mut self, slot: EdgeOf<Self>) {
let object = slot.load();
let new_object = self.trace_object(object);
debug_assert!(!self.plan.is_object_in_nursery(new_object));
slot.store(new_object);
slot.update_for_forwarding(|object| {
debug_assert!(!object.is_null());
let new_object = self.trace_object(object);
debug_assert!(!new_object.is_null());
debug_assert!(!self.plan.is_object_in_nursery(new_object));
new_object
});
}

fn create_scan_work(
Expand Down
1 change: 1 addition & 0 deletions src/policy/copyspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ impl<VM: VMBinding> CopySpace<VM> {
worker: &mut GCWorker<VM>,
) -> ObjectReference {
trace!("copyspace.trace_object(, {:?}, {:?})", object, semantics,);
debug_assert!(!object.is_null());

// If this is not from space, we do not need to trace it (the object has been copied to the tosapce)
if !self.is_from_space() {
Expand Down
1 change: 1 addition & 0 deletions src/policy/immix/immixspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ impl<VM: VMBinding> crate::policy::gc_work::PolicyTraceObject<VM> for ImmixSpace
copy: Option<CopySemantics>,
worker: &mut GCWorker<VM>,
) -> ObjectReference {
debug_assert!(!object.is_null());
if KIND == TRACE_KIND_TRANSITIVE_PIN {
self.trace_object_without_moving(queue, object)
} else if KIND == TRACE_KIND_DEFRAG {
Expand Down
1 change: 1 addition & 0 deletions src/policy/immortalspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ impl<VM: VMBinding> ImmortalSpace<VM> {
queue: &mut Q,
object: ObjectReference,
) -> ObjectReference {
debug_assert!(!object.is_null());
#[cfg(feature = "vo_bit")]
debug_assert!(
crate::util::metadata::vo_bit::is_vo_bit_set::<VM>(object),
Expand Down
1 change: 1 addition & 0 deletions src/policy/largeobjectspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ impl<VM: VMBinding> LargeObjectSpace<VM> {
queue: &mut Q,
object: ObjectReference,
) -> ObjectReference {
debug_assert!(!object.is_null());
#[cfg(feature = "vo_bit")]
debug_assert!(
crate::util::metadata::vo_bit::is_vo_bit_set::<VM>(object),
Expand Down
1 change: 1 addition & 0 deletions src/policy/markcompactspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ impl<VM: VMBinding> crate::policy::gc_work::PolicyTraceObject<VM> for MarkCompac
_copy: Option<CopySemantics>,
_worker: &mut GCWorker<VM>,
) -> ObjectReference {
debug_assert!(!object.is_null());
debug_assert!(
KIND != TRACE_KIND_TRANSITIVE_PIN,
"MarkCompact does not support transitive pin trace."
Expand Down
4 changes: 1 addition & 3 deletions src/policy/marksweepspace/malloc_ms/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,7 @@ impl<VM: VMBinding> MallocSpace<VM> {
queue: &mut Q,
object: ObjectReference,
) -> ObjectReference {
if object.is_null() {
return object;
}
debug_assert!(!object.is_null());

assert!(
self.in_space(object),
Expand Down
4 changes: 1 addition & 3 deletions src/policy/marksweepspace/native_ms/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,7 @@ impl<VM: VMBinding> MarkSweepSpace<VM> {
queue: &mut Q,
object: ObjectReference,
) -> ObjectReference {
if object.is_null() {
return object;
}
debug_assert!(!object.is_null());
debug_assert!(
self.in_space(object),
"Cannot mark an object {} that was not alloced by free list allocator.",
Expand Down
38 changes: 22 additions & 16 deletions src/scheduler/gc_work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,11 +662,16 @@ pub trait ProcessEdgesWork:
/// Process an edge, including loading the object reference from the memory slot,
/// trace the object and store back the new object reference if necessary.
fn process_edge(&mut self, slot: EdgeOf<Self>) {
let object = slot.load();
let new_object = self.trace_object(object);
if Self::OVERWRITE_REFERENCE {
slot.store(new_object);
}
slot.update_for_forwarding(|object| {
debug_assert!(!object.is_null());
let new_object = self.trace_object(object);
debug_assert!(!new_object.is_null());
if Self::OVERWRITE_REFERENCE {
new_object
} else {
ObjectReference::NULL
}
});
}

/// Process all the edges in the work packet.
Expand Down Expand Up @@ -721,9 +726,7 @@ impl<VM: VMBinding> ProcessEdgesWork for SFTProcessEdges<VM> {
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
use crate::policy::sft::GCWorkerMutRef;

if object.is_null() {
return object;
}
debug_assert!(!object.is_null());

// Erase <VM> type parameter
let worker = GCWorkerMutRef::new(self.worker());
Expand Down Expand Up @@ -997,21 +1000,24 @@ impl<VM: VMBinding, P: PlanTraceObject<VM> + Plan<VM = VM>, const KIND: TraceKin
}

fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
if object.is_null() {
return object;
}
debug_assert!(!object.is_null());
// We cannot borrow `self` twice in a call, so we extract `worker` as a local variable.
let worker = self.worker();
self.plan
.trace_object::<VectorObjectQueue, KIND>(&mut self.base.nodes, object, worker)
}

fn process_edge(&mut self, slot: EdgeOf<Self>) {
let object = slot.load();
let new_object = self.trace_object(object);
if P::may_move_objects::<KIND>() {
slot.store(new_object);
}
slot.update_for_forwarding(|object| {
debug_assert!(!object.is_null());
let new_object = self.trace_object(object);
debug_assert!(!new_object.is_null());
if P::may_move_objects::<KIND>() {
new_object
} else {
ObjectReference::NULL
}
});
}
}

Expand Down
61 changes: 61 additions & 0 deletions src/vm/edge_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,72 @@ use crate::util::{Address, ObjectReference};
/// in a word as a pointer, it can also use `SimpleEdge` for weak reference fields.
pub trait Edge: Copy + Send + Debug + PartialEq + Eq + Hash {
/// Load object reference from the edge.
///
/// If the slot is not holding an object reference, it should return `ObjectReference::NULL`.
/// Specifically,
///
/// - If the langauge has the concept of "null pointer" which does not point to any object in
/// the heap, this method should return `ObjectReference::NULL` regardless how a null
/// pointer is encoded in the VM. However, if the VM uses a special object in the heap to
/// represent a null value, such as the `None` object of `NoneType` in Python, this method
/// should still return the object reference to such `None` objects so that they are
/// properly traced, kept alive, and they have their references forwarded.
/// - If, in a VM, the data type a slot can hold is a union of references and non-reference
/// values, and the slot is currently holding a non-reference value, such as a small
/// integer, a floating-point number, or any special value such as `true`, `false` or `nil`
/// that do not point to any object, the slot is considered not holding an reference. This
/// method should return `ObjectReference::NULL` in such cases.
///
/// If the slot holds an object reference with tag bits, the returned value shall be the object
/// reference with the tag bits removed.
fn load(&self) -> ObjectReference;

/// Store the object reference `object` into the edge.
///
/// FIXME: Currently the subsuming write barrier (`Barrier::object_reference_write`) calls this
/// method to perform the actual store to the field. It only works if the VM does not store
/// tag bits in the slot.
///
/// FIXME: If the slot contains tag bits, consider overriding the `update_for_forwarding`
/// method. See: https://github.com/mmtk/mmtk-core/issues/1033
fn store(&self, object: ObjectReference);

/// Update the slot for forwarding.
///
/// If the slot is holding an object reference, this method shall call `updater` with that
/// reference; if the slot is not holding an object reference, including when holding a NULL
/// pointer or holding small integers or special non-reference values such as `true`, `false`
/// or `nil`, this method does not need to take further action. In no circumstance should this
/// method pass `ObjectReference::NULL` to `updater`.
///
/// If the returned value of the `updater` closure is not `ObjectReference::NULL`, it will be
/// the forwarded object reference of the original object, and this method shall update the
/// slot to point to the new location; if the returned value is `ObjectReference::NULL`, this
/// method does not need to take further action.
///
/// This method is called to trace the object pointed by this slot, and forward the reference
/// in the slot. To implement this semantics, the VM should usually preserve the tag bits if
/// it uses tagged pointers to indicate whether the slot holds an object reference or
/// non-reference values.
///
/// The default implementation calls `self.load()` to load the object reference, and calls
/// `self.store` to store the updated reference. VMs that use tagged pointers should override
/// this method to preserve the tag bits between the load and store.
fn update_for_forwarding<F>(&self, updater: F)
where
F: FnOnce(ObjectReference) -> ObjectReference,
{
let object = self.load();
if object.is_null() {
return;
}
let new_object = updater(object);
if new_object.is_null() {
Copy link
Collaborator

@k-sareen k-sareen Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do new_object.is_null() && object == new_object? That would fix the case where we overwrite references for objects that were not moved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am expecting the updater (usually implemented by trace_object) to do that test inside (if necessary at all) so that we don't need to check object == new_object here. One example is the nursery (for GenCopy and GenImmix). It always moves the object, so it doesn't need to check. It actually does debug_assert!(!self.plan.is_object_in_nursery(new_object)); which implies new_object cannot be equal to object.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm? I don't follow. trace_object can't store the new object (if any) into the slot. It will only return an object reference

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. trace_object doesn't do the storing. However, the contract of the updater closure is, if it returns ObjectReference::NULL, then Edge::update_for_forwarding will not do the storing.

process_edge implements the updater closure. After executing let new_object = trace_object(object), it should check if new_object == object and, if they are equal, return ObjectReference::NULL from the updater closure. Currently ProcessEdgesWork::process_edge is not doing this because I don't want to change the semantics of the existing ProcessEdgesWork::process_edge. We may give it a try by adding if new_object == object { return ObjectReference::NULL; } else { return new_object; } there. IIRC, @qinsoon once tried doing this check after trace_object, and it is not always profitable. We may give it another try, but at this moment, I want to make sure the closure itself doesn't introduce extra cost.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe Yi's work made it an Option<ObjectReference>. That has more overhead since it doesn't condense into a usize (given ObjectReference::NULL exists). I don't think we should retain the current semantics since we know the current semantics are broken. There is no need to store the same object again into the slot

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related issues:

My previous experiment showed that the performance impact is negligible. So we have reasons to replace the constant OVERWRITE_REFERENCE and the invocation P::may_move_objects::<KIND>() with an actual new_object == object check. But we'll probably do it in a separate PR to address #574 specifically.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Alternatively, we can make a PR for fixing that right now (since it should be simple to fix) and then merge that first, given this PR is a "design" PR and I'm not sure if we've decided what's the best way to review them yet

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Alternatively, we can make a PR for fixing that right now (since it should be simple to fix) and then merge that first

Yes. That's a good idea.

given this PR is a "design" PR and I'm not sure if we've decided what's the best way to review them yet

I think this can be an "MEP", too, but this may not be as controversal as removing ObjectReference::NULL. Since I already made this PR, I think I will test the performance with the OpenJDK binding and, if it performas well, I'll write up an "MEP" for this, too, and welcome everyone to discuss. Actually I still have some design questions about this. One is whether we need a way to update slots atomically. The LXR branch sometimes does "compare exchange" on slots.

return;
}
self.store(new_object);
}

/// Prefetch the edge so that a subsequent `load` will be faster.
fn prefetch_load(&self) {
// no-op by default
Expand Down
Loading