diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-16-12-02-26.gh-issue-129210.a5uLpE.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-16-12-02-26.gh-issue-129210.a5uLpE.rst new file mode 100644 index 00000000000000..85041c26c1b0e7 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-16-12-02-26.gh-issue-129210.a5uLpE.rst @@ -0,0 +1,2 @@ +Optimise garbage collection in case where no objects to collect have +finalizers, and hence cannot be resurrected. diff --git a/Python/gc.c b/Python/gc.c index dad088e09f872f..198f881fac50dc 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -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 @@ -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); @@ -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 @@ -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. */ diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index fa4cb56f01e800..e9a766fa338de9 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -1592,12 +1592,14 @@ 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)) { @@ -1605,10 +1607,12 @@ finalize_garbage(struct collection_state *state) 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. @@ -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);