From 8a5bd9a55aae06a200fbdbf90fd6f830c9f39f5f Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sat, 10 Oct 2026 20:00:29 +0100 Subject: [PATCH] gh-159137: Skip redundant GC resurrection analysis --- Include/internal/pycore_genobject.h | 9 ++++ ...10-10-00-00-00.gh-issue-159137.gcReuse.rst | 4 ++ Python/gc.c | 32 ++++++++++-- Python/gc_free_threading.c | 51 +++++++++++++------ 4 files changed, 76 insertions(+), 20 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-00-00-00.gh-issue-159137.gcReuse.rst diff --git a/Include/internal/pycore_genobject.h b/Include/internal/pycore_genobject.h index 266add0fb7a9c34..6de917de54425be 100644 --- a/Include/internal/pycore_genobject.h +++ b/Include/internal/pycore_genobject.h @@ -8,6 +8,7 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif +#include "pycore_frame.h" // FRAME_STATE_FINISHED() #include "pycore_interpframe_structs.h" // _PyGenObject #include // offsetof() @@ -21,6 +22,14 @@ PyGenObject *_PyGen_GetGeneratorFromFrame(_PyInterpreterFrame *frame) return (PyGenObject *)(((char *)frame) - offset_in_gen); } +// The generator finalizer is a no-op once its frame is cleared. +static inline int +_PyGen_FinalizerIsNoop(PyObject *op) +{ + return Py_TYPE(op)->tp_finalize == PyGen_Type.tp_finalize && + FRAME_STATE_FINISHED(((PyGenObject *)op)->gi_frame_state); +} + PyAPI_FUNC(PyObject *)_PyGen_yf(PyGenObject *); extern int _PyGen_ClearFrame(PyGenObject *self); diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-00-00-00.gh-issue-159137.gcReuse.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-00-00-00.gh-issue-159137.gcReuse.rst new file mode 100644 index 000000000000000..9e03fe30e1d5b15 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-00-00-00.gh-issue-159137.gcReuse.rst @@ -0,0 +1,4 @@ +Avoid a second reachability analysis in the garbage collector when no weakref +callback, finalizer, or collectable-object debug output ran. Finished generators, +coroutines, and async generators can skip this analysis even when finalized. +Free-threaded builds also avoid a second stop-the-world pause in this case. diff --git a/Python/gc.c b/Python/gc.c index bb20dae5a6543fa..5ce94819ae3f608 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -5,6 +5,7 @@ #include "Python.h" #include "pycore_ceval.h" // _Py_set_eval_breaker_bit() #include "pycore_dict.h" // _PyInlineValuesSize() +#include "pycore_genobject.h" // _PyGen_FinalizerIsNoop() #include "pycore_initconfig.h" // _PyStatus_OK() #include "pycore_context.h" #include "pycore_interp.h" // PyInterpreterState.gc @@ -805,7 +806,8 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers) * Python-level code. See bpo-38006 as an example bug. */ static int -handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old) +handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, + bool *needs_resurrection_check) { PyGC_Head *gc; PyGC_Head wrcb_to_call; /* weakrefs with callbacks to call */ @@ -906,6 +908,9 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old) /* Invoke the callbacks we decided to honor. It's safe to invoke them * because they can't reference unreachable objects. */ + if (!*needs_resurrection_check) { + *needs_resurrection_check = !gc_list_is_empty(&wrcb_to_call); + } while (! gc_list_is_empty(&wrcb_to_call)) { PyObject *temp; PyObject *callback; @@ -1043,7 +1048,8 @@ handle_legacy_finalizers(PyThreadState *tstate, * list, due to refcounts falling to 0. */ static void -finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable) +finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable, + bool *needs_resurrection_check) { destructor finalize; PyGC_Head seen; @@ -1065,6 +1071,9 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable) if (!_PyGC_FINALIZED(op) && (finalize = Py_TYPE(op)->tp_finalize) != NULL) { + if (!_PyGen_FinalizerIsNoop(op)) { + *needs_resurrection_check = true; + } _PyGC_SET_FINALIZED(op); Py_INCREF(op); finalize(op); @@ -1550,26 +1559,39 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) validate_list(&finalizers, collecting_clear_unreachable_clear); validate_list(&unreachable, collecting_set_unreachable_clear); + bool needs_resurrection_check = false; + /* Print debugging information. */ if (gcstate->debug & _PyGC_DEBUG_COLLECTABLE) { + /* sys.stderr.write() can resurrect objects. */ + needs_resurrection_check = true; for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) { debug_cycle("collectable", FROM_GC(gc)); } } /* Clear weakrefs and invoke callbacks as necessary. */ - stats.collected += handle_weakref_callbacks(&unreachable, old); + stats.collected += handle_weakref_callbacks(&unreachable, old, + &needs_resurrection_check); validate_list(old, collecting_clear_unreachable_clear); validate_list(&unreachable, collecting_set_unreachable_clear); /* Call tp_finalize on objects which have one. */ - finalize_garbage(tstate, &unreachable); + finalize_garbage(tstate, &unreachable, &needs_resurrection_check); /* 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; - handle_resurrected_objects(&unreachable, &final_unreachable, old); + if (needs_resurrection_check) { + handle_resurrected_objects(&unreachable, &final_unreachable, old); + } + else { + /* No Python code ran during finalization, so the original + * unreachable set is still valid. */ + gc_list_init(&final_unreachable); + gc_list_merge(&unreachable, &final_unreachable); + } /* Clear weakrefs to objects in the unreachable set. No Python-level * code must be allowed to access those unreachable objects. During diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 42a28b8fb2a5ee3..adf73d79a46661b 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -1542,11 +1542,19 @@ move_legacy_finalizer_reachable(struct collection_state *state) // or missing `tp_traverse` methods. When that object goes away, the callback // for weakref can be executed and that could reveal unreachable objects to // Python-level code. See bpo-38006 as an example bug. -static void -find_weakref_callbacks(struct collection_state *state) +static bool +prepare_finalization(struct collection_state *state, + bool *needs_resurrection_check) { + bool has_finalizers = false; PyObject *op; WORKSTACK_FOR_EACH(&state->unreachable, op) { + if (!_PyGC_FINALIZED(op) && Py_TYPE(op)->tp_finalize != NULL) { + has_finalizers = true; + if (!_PyGen_FinalizerIsNoop(op)) { + *needs_resurrection_check = true; + } + } if (!_PyType_SUPPORTS_WEAKREFS(Py_TYPE(op))) { continue; } @@ -1590,12 +1598,14 @@ find_weakref_callbacks(struct collection_state *state) // Enqueue weakref to be called later. worklist_push(&state->wrcb_to_call, (PyObject *)wr); + *needs_resurrection_check = true; } } + return has_finalizers; } // Clear weakrefs to objects in the unreachable set. See comments -// above find_weakref_callbacks() for why this clearing is required. +// above prepare_finalization() for why this clearing is required. static void clear_weakrefs(struct collection_state *state) { @@ -2132,29 +2142,40 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state, gc_visit_heaps(interp, &validate_alive_bits, &state->base); #endif + // Deallocations after the refcount merge may run Python code. + bool needs_resurrection_check = state->objs_to_decref.head != 0; + // Print debugging information. if (interp->gc.debug & _PyGC_DEBUG_COLLECTABLE) { + needs_resurrection_check = true; PyObject *op; WORKSTACK_FOR_EACH(&state->unreachable, op) { debug_cycle("collectable", op); } } - // Find weakref callbacks we will honor (but do not call them). - find_weakref_callbacks(state); - _PyEval_StartTheWorld(interp); + // Find weakref callbacks and pending finalizers (but do not call them). + bool has_finalizers = prepare_finalization(state, &needs_resurrection_check); + if (needs_resurrection_check) { + _PyEval_StartTheWorld(interp); - // Deallocate any object from the refcount merge step - cleanup_worklist(&state->objs_to_decref); + // Deallocate any object from the refcount merge step + cleanup_worklist(&state->objs_to_decref); - // Call weakref callbacks and finalizers after unpausing other threads to - // avoid potential deadlocks. - call_weakref_callbacks(state); - finalize_garbage(state); + // Call weakref callbacks and finalizers after unpausing other threads to + // avoid potential deadlocks. + call_weakref_callbacks(state); + finalize_garbage(state); - _PyEval_StopTheWorld(interp); - // Handle any objects that may have resurrected after the finalization. - err = handle_resurrected_objects(state); + _PyEval_StopTheWorld(interp); + // Handle any objects that may have resurrected after the finalization. + err = handle_resurrected_objects(state); + } + else if (has_finalizers) { + // Only no-op generator finalizers remain; keep other threads paused. + finalize_garbage(state); + } + // Otherwise, keep other threads paused so the unreachable set stays valid. // Clear free lists in all threads _PyGC_ClearAllFreeLists(interp); if (err == 0) {