https://github.com/python/cpython/commit/e878e1e9688ebaafbfae700a872fa30a9f86a4c4
commit: e878e1e9688ebaafbfae700a872fa30a9f86a4c4
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-10-11T16:01:50Z
summary:

gh-159137: Untrack tuples and count survivors in move_unreachable() (#159154)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-23-50-00.gh-issue-159137.gcWalk.rst
M Python/gc.c

diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-23-50-00.gh-issue-159137.gcWalk.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-23-50-00.gh-issue-159137.gcWalk.rst
new file mode 100644
index 00000000000000..85cdfb028ae08f
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-23-50-00.gh-issue-159137.gcWalk.rst
@@ -0,0 +1,3 @@
+Speed up the garbage collector by untracking tuples and counting the surviving
+objects while it looks for unreachable objects, which saves two walks of the
+collected generation in every collection.
diff --git a/Python/gc.c b/Python/gc.c
index 5ce94819ae3f60..5dc79e20191ebc 100644
--- a/Python/gc.c
+++ b/Python/gc.c
@@ -576,9 +576,10 @@ visit_reachable(PyObject *op, void *arg)
  * But _gc_next in unreachable list has NEXT_MASK_UNREACHABLE flag.
  * So we can not gc_list_* functions for unreachable until we remove the flag.
  */
-static void
+static Py_ssize_t
 move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
 {
+    Py_ssize_t reachable = 0;
     // previous elem in the young list, used for restore gc_prev.
     PyGC_Head *prev = young;
     PyGC_Head *gc = GC_NEXT(young);
@@ -615,7 +616,21 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
             _PyGCHead_SET_PREV(gc, prev);
             // gc is not COLLECTING state after here.
             gc_clear_collecting(gc);
-            prev = gc;
+            if (PyTuple_CheckExact(op)) {
+                // Untracking gc overwrites the gc_refs of the next object.
+                PyGC_Head *next = GC_NEXT(gc);
+                uintptr_t next_refs = next->_gc_prev;
+                _PyTuple_MaybeUntrack(op);
+                next->_gc_prev = next_refs;
+                if (gc->_gc_next != 0) {
+                    prev = gc;
+                    reachable++;
+                }
+            }
+            else {
+                prev = gc;
+                reachable++;
+            }
         }
         else {
             /* This *may* be unreachable.  To make progress,
@@ -648,32 +663,7 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
     young->_gc_prev = (uintptr_t)prev;
     // don't let the pollution of the list head's next pointer leak
     unreachable->_gc_next &= ~NEXT_MASK_UNREACHABLE;
-}
-
-/* In theory, all tuples should be younger than the
-* objects they refer to, as tuples are immortal.
-* Therefore, untracking tuples in oldest-first order in the
-* young generation before promoting them should have tracked
-* all the tuples that can be untracked.
-*
-* Unfortunately, the C API allows tuples to be created
-* and then filled in. So this won't untrack all tuples
-* that can be untracked. It should untrack most of them
-* and is much faster than a more complex approach that
-* would untrack all relevant tuples.
-*/
-static void
-untrack_tuples(PyGC_Head *head)
-{
-    PyGC_Head *gc = GC_NEXT(head);
-    while (gc != head) {
-        PyObject *op = FROM_GC(gc);
-        PyGC_Head *next = GC_NEXT(gc);
-        if (PyTuple_CheckExact(op)) {
-            _PyTuple_MaybeUntrack(op);
-        }
-        gc = next;
-    }
+    return reachable;
 }
 
 /* Return true if object has a pre-PEP 442 finalization method. */
@@ -1175,7 +1165,8 @@ flag is cleared (for example, by using 
'clear_unreachable_mask' function or
 by a call to 'move_legacy_finalizers'), the 'unreachable' list is not a normal
 list and we can not use most gc_list_* functions for it. */
 static inline Py_ssize_t
-deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
+deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable,
+                   Py_ssize_t *reachable) {
     validate_list(base, collecting_clear_unreachable_clear);
     /* Using ob_refcnt and gc_refs, calculate which objects in the
      * container set are reachable from outside the set (i.e., have a
@@ -1221,7 +1212,8 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head 
*unreachable) {
      * worth complicating the code to speed just a little.
      */
     gc_list_init(unreachable);
-    move_unreachable(base, unreachable);  // gc_prev is pointer again
+    // gc_prev is pointer again
+    *reachable = move_unreachable(base, unreachable);
     validate_list(base, collecting_clear_unreachable_clear);
     validate_list(unreachable, collecting_set_unreachable_set);
     return candidates;
@@ -1252,7 +1244,8 @@ handle_resurrected_objects(PyGC_Head *unreachable, 
PyGC_Head* still_unreachable,
     // have the PREV_MARK_COLLECTING set, but the objects are going to be
     // removed so we can skip the expense of clearing the flag.
     PyGC_Head* resurrected = unreachable;
-    deduce_unreachable(resurrected, still_unreachable);
+    Py_ssize_t reachable;
+    deduce_unreachable(resurrected, still_unreachable, &reachable);
     clear_unreachable_mask(still_unreachable);
 
     // Move the resurrected objects to the old generation for future 
collection.
@@ -1521,13 +1514,13 @@ gc_collect_main(PyThreadState *tstate, int generation, 
_PyGC_Reason reason)
     }
     validate_list(old, collecting_clear_unreachable_clear);
 
-    stats.candidates = deduce_unreachable(young, &unreachable);
+    Py_ssize_t reachable;
+    stats.candidates = deduce_unreachable(young, &unreachable, &reachable);
 
-    untrack_tuples(young);
     /* Move reachable objects to next generation. */
     if (young != old) {
         if (generation == NUM_GENERATIONS - 2) {
-            gcstate->long_lived_pending += gc_list_size(young);
+            gcstate->long_lived_pending += reachable;
         }
         gc_list_merge(young, old);
     }
@@ -1540,7 +1533,7 @@ gc_collect_main(PyThreadState *tstate, int generation, 
_PyGC_Reason reason)
         // site; the cost is slightly more work for full collections on dicts
         // with only atomic values.
         gcstate->long_lived_pending = 0;
-        gcstate->long_lived_total = gc_list_size(young);
+        gcstate->long_lived_total = reachable;
     }
 
     /* All objects in unreachable are trash, but objects reachable from

_______________________________________________
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