https://github.com/python/cpython/commit/fbbf4f48c48cd9de2373494b2408c1424fe7ef3f
commit: fbbf4f48c48cd9de2373494b2408c1424fe7ef3f
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-10-10T22:46:49Z
summary:

gh-159137: Skip redundant GC resurrection analysis (#159138)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-00-00-00.gh-issue-159137.gcReuse.rst
M Include/internal/pycore_genobject.h
M Python/gc.c
M Python/gc_free_threading.c

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 <stddef.h>               // 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) {

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to