Skip to content

gh-129210: GC optimisations for case where no objects being collected have finalizers #132488

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Optimise garbage collection in case where no objects to collect have
finalizers, and hence cannot be resurrected.
31 changes: 22 additions & 9 deletions Python/gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1073,12 +1073,14 @@ handle_legacy_finalizers(PyThreadState *tstate,
/* Run first-time finalizers (if any) on all the objects in collectable.
* Note that this may remove some (or even all) of the objects from the
* list, due to refcounts falling to 0.
* Return 1 if any finalizers were run.
*/
static void
static int
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
{
destructor finalize;
PyGC_Head seen;
int finalizer_run = 0;

/* While we're going through the loop, `finalize(op)` may cause op, or
* other objects, to be reclaimed via refcounts falling to zero. So
Expand All @@ -1097,6 +1099,7 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
if (!_PyGC_FINALIZED(op) &&
(finalize = Py_TYPE(op)->tp_finalize) != NULL)
{
finalizer_run = 1;
_PyGC_SET_FINALIZED(op);
Py_INCREF(op);
finalize(op);
Expand All @@ -1105,6 +1108,7 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
}
}
gc_list_merge(&seen, collectable);
return finalizer_run;
}

/* Break reference cycles by clearing the containers involved. This is
Expand Down Expand Up @@ -1736,20 +1740,29 @@ gc_collect_region(PyThreadState *tstate,
validate_list(&unreachable, collecting_set_unreachable_clear);

/* Call tp_finalize on objects which have one. */
finalize_garbage(tstate, &unreachable);
/* Handle any objects that may have resurrected after the call
* to 'finalize_garbage' and continue the collection with the
* objects that are still unreachable */
int check_resurrected = finalize_garbage(tstate, &unreachable);

/* If no finalizers have run, no objects can have been resurrected: in that
* case skip the resurrection check and just use the existing unreachable
* list. Otherwise we need to check and handle any objects that may have
* resurrected after the call to 'finalize_garbage' and continue the
* collection with the objects that are still unreachable. */
PyGC_Head final_unreachable;
gc_list_init(&final_unreachable);
handle_resurrected_objects(&unreachable, &final_unreachable, to);
PyGC_Head *to_delete;
if (check_resurrected) {
gc_list_init(&final_unreachable);
handle_resurrected_objects(&unreachable, &final_unreachable, to);
to_delete = &final_unreachable;
} else {
to_delete = &unreachable;
}

/* Call tp_clear on objects in the final_unreachable set. This will cause
* the reference cycles to be broken. It may also cause some objects
* in finalizers to be freed.
*/
stats->collected += gc_list_size(&final_unreachable);
delete_garbage(tstate, gcstate, &final_unreachable, to);
stats->collected += gc_list_size(to_delete);
delete_garbage(tstate, gcstate, to_delete, to);

/* Collect statistics on uncollectable objects found and print
* debugging information. */
Expand Down
14 changes: 10 additions & 4 deletions Python/gc_free_threading.c
Original file line number Diff line number Diff line change
Expand Up @@ -1592,23 +1592,27 @@ debug_cycle(const char *msg, PyObject *op)
/* Run first-time finalizers (if any) on all the objects in collectable.
* Note that this may remove some (or even all) of the objects from the
* list, due to refcounts falling to 0.
* Return 1 if any finalizers were run.
*/
static void
static int
finalize_garbage(struct collection_state *state)
{
// NOTE: the unreachable worklist holds a strong reference to the object
// to prevent it from being deallocated while we are holding on to it.
int finalizer_run = 0;
PyObject *op;
WORKSTACK_FOR_EACH(&state->unreachable, op) {
if (!_PyGC_FINALIZED(op)) {
destructor finalize = Py_TYPE(op)->tp_finalize;
if (finalize != NULL) {
_PyGC_SET_FINALIZED(op);
finalize(op);
finalizer_run = 1;
assert(!_PyErr_Occurred(_PyThreadState_GET()));
}
}
}
return finalizer_run;
}

// Break reference cycles by clearing the containers involved.
Expand Down Expand Up @@ -2009,11 +2013,13 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state,
// Call weakref callbacks and finalizers after unpausing other threads to
// avoid potential deadlocks.
call_weakref_callbacks(state);
finalize_garbage(state);
int check_resurrected = finalize_garbage(state);

// Handle any objects that may have resurrected after the finalization.
_PyEval_StopTheWorld(interp);
err = handle_resurrected_objects(state);
// Handle any objects that may have resurrected after finalization, if any
if (check_resurrected) {
err = handle_resurrected_objects(state);
}
// Clear free lists in all threads
_PyGC_ClearAllFreeLists(interp);
_PyEval_StartTheWorld(interp);
Expand Down
Loading