GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — gh-112529: Implement GC for free-threaded builds by colesbury · Pull Request #114262 · python/cpython Conversation
|
|
@DinoV, @pablogsal, @nascheme - I think this is ready for review now. There is duplicate code between I'll put up a PR for the devguide as well. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
The design, structure and code style look good to me. A couple of minor suggestions. Next to the definition of ob_tid, I think it should also mention that this field is (ab)used for the linked-list and for the GC refs count. Instead of I think using the marking stack is okay, despite the extra allocations needed. If it is turns out to be a problem, e.g. a program with a very deeply linked object structure, a fairly simple fix is as follows. Limit the size of the stack. If the size is exceeded, re-start the marking process. An additional refinement is to have a "already marked" bit on each mimalloc page and that way if you re-start, those pages can be quickly skipped. Note that the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Thanks for the review Neil. I've renamed
Yeah, that makes sense.
In a subsequent change (not yet a PR), I'm using |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| } | ||
|
|
||
| static int | ||
| gc_visit_heaps_locked(PyInterpreterState *interp, mi_block_visit_fun *visitor, |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Do we want to call this gc_visit_heaps_lock_held per our discussion on naming these style of functions?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| if (_PyObject_GC_IS_TRACKED(op) && !_Py_IsImmortal(op)) { | ||
| // If update_refs hasn't reached this object yet, mark it | ||
| // as (tentatively) unreachable and initialize ob_tid to zero. | ||
| if (!gc_is_unreachable(op)) { |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice to factor this into a helper like gc_init_refs or gc_maybe_init_refs that is used here and in update_refs
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| if (PyTuple_CheckExact(op)) { | ||
| _PyTuple_MaybeUntrack(op); | ||
| if (!_PyObject_GC_IS_TRACKED(op)) { | ||
| if (gc_is_unreachable(op)) { |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This could similarly be a gc_restore_refs or gc_maybe_restore_refs and used below
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| return NULL; | ||
| } | ||
|
|
||
| // Append all objects to a worklist. This abuses ob_tid. We will restore |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Is it worth stopping the world here? It seems like we're fine to race with ob_tid from a correctness stand point, but it seems like a lot of objects are going to end up with merged ref counts in a multithreaded environment!
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
We need to stop the world for the heap traversal (gc_visit_heaps) to be thread-safe... which I forgot to add here. I'll also add an assert to gc_visit_heaps() that the world is stopped.
This doesn't merge most refcounts. They get restored from the mimalloc data structure. They'll only get merged if the owning thread has already exited.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| // it later. NOTE: We can't append to the list during gc_visit_heaps() | ||
| // because PyList_Append() may reclaim an abandoned mimalloc segment | ||
| // while we are traversing it. | ||
| struct get_objects_args args = { 0 }; |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Ditto to get referrers
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
| op->ob_tid = 0; | ||
| op->ob_ref_local = 0; | ||
| op->ob_ref_shared = _Py_REF_SHARED(refcount, _Py_REF_MERGED); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
It seems like this needs to be an atomic operation? Can this just be a call to _Py_ExplicitMergeRefcount?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
The other threads in the interpreter must be paused, so no atomics necessary. I'll add an assert here.
We could probably use _Py_ExplicitMergeRefcount(), but it seems convenient to avoid the atomic operations.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
| // Clear weakrefs and enqueue callbacks (but do not call them). | ||
| clear_weakrefs(state); | ||
| _PyEval_StartTheWorld(interp); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
So I think if I understand this correctly what's going to happen with the thread ID and any reference count changes that may happen at this point, but the assumption is that the thread ID is never going to actually clash with an object reference in the work list. And then anything which does have a reference count operation will just become merged when we restore it?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the key assumption is that _PyThread_Id() is itself a pointer to a distinct object so never conflicts with an object in the worklists. The implementation of _PyThread_Id() uses the address of the platform's thread control block or equivalent.
On the GC side, we abuse ob_tid for two purposes:
gc_refs, which can have arbitrary values (might conflict!), but this is only happens while other threads are paused, so nobody sees it.worklist pointers, which must not conflict with _PyThread_Id()Some worklists are only used while other threads are paused (e.g., in _PyGC_GetObjects()). Other worklists are created when threads are paused, but still used while other threads may be running (e.g., unreachable, wrcb_to_call).
If the worklist continues to be used while other threads are running, then there are two other important considerations:
tp_clear).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Here's the PR so far for the devguide: python/devguide#1263 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
* pythongh-112529: Implement GC for free-threaded builds This implements a mark and sweep GC for the free-threaded builds of CPython. The implementation relies on mimalloc to find GC tracked objects (i.e., "containers").
* pythongh-112529: Implement GC for free-threaded builds This implements a mark and sweep GC for the free-threaded builds of CPython. The implementation relies on mimalloc to find GC tracked objects (i.e., "containers").
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.