feat: implement mark_sweep _branded based on approved API redesign#83
feat: implement mark_sweep _branded based on approved API redesign#83shruti2522 wants to merge 7 commits intoboa-dev:mainfrom
Conversation
5ed16ac to
a7869b7
Compare
nekevss
left a comment
There was a problem hiding this comment.
Some early feedback. I may try a second pass at this within a couple days.
In general, a lot of this is super interesting. Super excited to see it progress 😄
| /// Use `Tracer::mark` for every reachable `Gc` pointer. | ||
| pub trait Trace { | ||
| /// Marks all `Gc` pointers reachable from `self`. | ||
| fn trace(&mut self, tracer: &mut Tracer); |
There was a problem hiding this comment.
Hmmmm, a couple questions here:
- Have you looked into using a Trace color vs. a
Tracer? I'd prefer that approach vs. keepingTraceraround. - This should still be
&vs.& mut.
There was a problem hiding this comment.
Yes, the TraceColor refactor slipped my mind 😅 on it now. Also switching &mut self to &self since mark bits already use interior mutability via Cell<bool> so it's overkill
| let slot = self | ||
| .pool | ||
| .borrow_mut() | ||
| .try_alloc_bytes(layout) |
There was a problem hiding this comment.
I think this is incorrect. It should be try_alloc not try_alloc_bytes
Is there a reason for allocating the the BumpPage here rather than the slots?
If we are using one of the bump pages here, then we should probably use one of the arena allocators
| // and we ensure that `Gc` objects and pool allocations do not outlive | ||
| // the `Collector` instance | ||
| pub(crate) pool: RefCell<PoolAllocator<'static>>, | ||
| pool_entries: RefCell<Vec<PoolEntry>>, |
There was a problem hiding this comment.
We shouldn't need to maintain a Vec of pool entries here nor the allocation count. That should be managed by the allocator.
| use core::ptr::NonNull; | ||
|
|
||
| /// A weak reference to a GC managed value | ||
| pub struct WeakGc<'id, T: Trace + ?Sized> { |
There was a problem hiding this comment.
Hmmmm, I think we will still need an ephemeron implementation, but we could probably iterate on this
| pub fn root<T: Trace + Finalize + 'gc>(&self, gc: Gc<'gc, T>) -> Root<'id, T> { | ||
| let gc_ptr = gc.ptr; | ||
|
|
||
| let node = Box::new(RootNode { |
There was a problem hiding this comment.
RootNode should ideally be allocated onto our heap. It would actually probably be really ideal to have a mempool specifically for RootNodes
| pub(crate) fn iter_from_sentinel( | ||
| sentinel: NonNull<Self>, | ||
| ) -> impl Iterator<Item = NonNull<Self>> { | ||
| struct Iter { |
There was a problem hiding this comment.
thought: this should probably just be a separate iterator RootLinkIter, and then we call iter that returns RootLinkIter rather than iter_from_sentinel call which is an anonymous iter
| // the `Collector` instance | ||
| pub(crate) pool: RefCell<PoolAllocator<'static>>, | ||
| pool_entries: RefCell<Vec<PoolEntry>>, | ||
| pub(crate) sentinel: Pin<Box<RootLink>>, |
There was a problem hiding this comment.
How about a new type RootSentinel that can be a transparent new type.
pub struct RootSentinel(Pin<Box<RootLink>>);| /// Reachability flag set by the mark phase. | ||
| pub(crate) marked: Cell<bool>, | ||
| /// Type-erased trace function. | ||
| pub(crate) trace_fn: Option<TraceFn>, |
There was a problem hiding this comment.
Is there ever an instance where trace_fn would be None. This should be fine to just be TraceFn as it's never not set.
57590c5 to
85b9952
Compare
nekevss
left a comment
There was a problem hiding this comment.
Have another small batch of comments / feedback
| let size = mem::size_of::<PoolItem<gc::RootNode<'id, T>>>(); | ||
| let mut pool = self.root_pool.borrow_mut(); | ||
| let slot_ptr = pool | ||
| .alloc_slot_raw(size) |
There was a problem hiding this comment.
Why are we creating new methods on pool rather than using the pre-existing try_alloc?
|
|
||
| let mut pool = self.pool.borrow_mut(); | ||
| let slot_ptr = pool | ||
| .alloc_slot_raw(size) |
There was a problem hiding this comment.
same question here: why would we be using a new alloc_slot_raw vs. the pre-existing try_alloc
| // Initialize the PoolItem<GcBox<T>> in place | ||
| core::ptr::write( | ||
| pool_item_ptr.as_ptr(), | ||
| PoolItem(GcBox { |
There was a problem hiding this comment.
This should be ideally be defined as a ctor on GcBox rather than created here.
|
|
||
| self.generic_alloc_id.set(alloc_id.wrapping_add(1)); | ||
|
|
||
| unsafe fn trace_value<T: trace::Trace>( |
There was a problem hiding this comment.
This definition can be moved to the gc_box module
| /// | ||
| /// The `'a` lifetime ties the color to the collection cycle, | ||
| /// preventing it from being stored or escaping the collector. | ||
| pub struct TraceColor<'a> { |
There was a problem hiding this comment.
This change just kept the tracer implementation and named it to TraceColor. Is there a reason for the name change vs. switching to the full tricolor approach?
Is the worklist somehow necessary for this current implementation?
e270d95 to
08de4ff
Compare
08de4ff to
f5d3154
Compare
nekevss
left a comment
There was a problem hiding this comment.
Hopefully last round of general review. 😅 There's a several things that I think I'd prefer to address on this first pass at this API
|
|
||
| /// A weak reference to a GC managed value | ||
| pub struct WeakGc<'id, T: Trace + ?Sized> { | ||
| pub(crate) ptr: NonNull<PoolItem<GcBox<T>>>, |
| use core::ptr::NonNull; | ||
|
|
||
| pub struct Ephemeron<'id, K: Trace, V: Trace> { | ||
| pub(crate) key_ptr: NonNull<PoolItem<GcBox<K>>>, |
There was a problem hiding this comment.
issue: key_ptr here should be an Option<pointer>
Ephemeron keys are removed and set to None when they underlying ptr is invalid (i.e. null).
| /// A transient pointer to a GC-managed value. | ||
| #[derive(Debug)] | ||
| pub struct Gc<'gc, T: Trace + ?Sized + 'gc> { | ||
| pub(crate) ptr: NonNull<PoolItem<GcBox<T>>>, |
There was a problem hiding this comment.
Here and elsewhere: is there a reason for using NonNull<PoolItem<GcBox<T>>>? If there's not a particular reason, then we should be using PoolPointer
| // the `Collector` instance | ||
| pub(crate) pool: RefCell<PoolAllocator<'static>>, | ||
| /// Dedicated pool for RootNode allocations | ||
| pub(crate) root_pool: RefCell<PoolAllocator<'static>>, |
There was a problem hiding this comment.
thought: hmmmm, intriguing. I'm not entirely convinced immediately that this is needed, but we can leave it as is currently.
| for link_ptr in self.sentinel.iter() { | ||
| unsafe { | ||
| // Read the `gc_ptr` field using the stable offset | ||
| let gc_ptr_ptr = link_ptr |
There was a problem hiding this comment.
Instead of doing pointer offsets here, we should be able to cast to an ErasedRootNode type and use that specific type to begin the marking correct? My initial reaction would is that may be a better approach as it allows us to be more explicit and not deal as much in NonNull<u8> land.
Is there any benefit to manually calculating the pointer and dealing with NonNull<u8>s over explicit types
| drop_and_free::<T>, | ||
| alloc_id, | ||
| )) | ||
| .expect("branded GC: pool allocation failed"); |
There was a problem hiding this comment.
We should be throwing this error and not panicking
| for entry in self.ephemerons.borrow().iter() { | ||
| use crate::alloc::mempool3::PoolItem; | ||
| unsafe { | ||
| let key_box = entry.key_ptr.cast::<PoolItem<GcBox<()>>>(); |
There was a problem hiding this comment.
thought: If we are always casting to this value, then we should just probably just store the pointer as that value.
| unsafe { | ||
| let key_box = entry.key_ptr.cast::<PoolItem<GcBox<()>>>(); | ||
| // Skip entries invalidated by a previous collection cycle. | ||
| if (*key_box.as_ptr()).0.alloc_id != entry.key_alloc_id { |
There was a problem hiding this comment.
This should be probably handled by the ephemeron key being an option
Implement
mark_sweep_brandedGC based on the approved API redesign for oscars. This is a draft implementation, also includes changes from #82