Skip to content

Commit 788521e

Browse files
committed
gh-159437: Skip traversal for untrackable tuple survivors
1 parent b70a512 commit 788521e

5 files changed

Lines changed: 73 additions & 30 deletions

File tree

‎Include/internal/pycore_tuple.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ extern "C" {
1111
#include "pycore_object.h" // _PyObject_GC_IS_TRACKED
1212
#include "pycore_structs.h" // _PyStackRef
1313

14+
/* Check whether an object is eligible for tuple untracking.
15+
* `op`: The live object to inspect.
16+
* Returns: True for an exact tracked tuple with no NULL or potentially tracked items.
17+
*/
18+
extern bool _PyTuple_CanUntrack(PyObject *op);
1419
extern void _PyTuple_MaybeUntrack(PyObject *);
1520
extern void _PyTuple_DebugMallocStats(FILE *out);
1621

‎Lib/test/test_gc.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,25 @@ def test_tuple(self):
107107
del l
108108
self.assertEqual(gc.collect(), 2)
109109

110+
@requires_gil_enabled("tests the generation-list collector")
111+
def test_unreachable_tuple_leaf_is_collected(self):
112+
"""Keep unreachable tuple leaves in the collected-object count."""
113+
was_enabled = gc.isenabled()
114+
gc.disable()
115+
self.addCleanup(gc.enable if was_enabled else gc.disable)
116+
gc.collect()
117+
118+
cycle = []
119+
# Iterator construction keeps this atomic-only tuple tracked until GC.
120+
leaf = tuple(iter((object(),)))
121+
cycle.extend((cycle, leaf))
122+
self.assertTrue(gc.is_tracked(leaf))
123+
124+
# The tuple must stay tracked until classification so both objects
125+
# contribute to the collected count.
126+
del cycle, leaf
127+
self.assertEqual(gc.collect(), 2)
128+
110129
def test_class(self):
111130
class A:
112131
pass
@@ -1289,6 +1308,21 @@ def test_heap_size(self):
12891308
del l
12901309
self.assertEqual(count, _testinternalcapi.get_tracked_heap_size())
12911310

1311+
@requires_gil_enabled('need generational GC')
1312+
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
1313+
def test_heap_size_after_tuple_untracking(self):
1314+
"""GC heap accounting reflects tuples untracked during collection."""
1315+
gc.collect()
1316+
count = _testinternalcapi.get_tracked_heap_size()
1317+
# Iterator-based construction initially tracks even atomic-only tuples.
1318+
value = tuple(iter((object(),)))
1319+
self.assertTrue(gc.is_tracked(value))
1320+
self.assertEqual(count + 1, _testinternalcapi.get_tracked_heap_size())
1321+
1322+
gc.collect()
1323+
self.assertFalse(gc.is_tracked(value))
1324+
self.assertEqual(count, _testinternalcapi.get_tracked_heap_size())
1325+
12921326

12931327
class GCCallbackTests(unittest.TestCase):
12941328
def setUp(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Garbage collection in builds with the GIL now skips reachability traversal for exact tuple survivors that can be untracked immediately.

‎Objects/tupleobject.c‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,14 @@ PyTuple_SetItem(PyObject *op, Py_ssize_t i, PyObject *newitem)
134134
return 0;
135135
}
136136

137-
void
138-
_PyTuple_MaybeUntrack(PyObject *op)
137+
bool
138+
_PyTuple_CanUntrack(PyObject *op)
139139
{
140140
PyTupleObject *t;
141141
Py_ssize_t i, n;
142142

143143
if (!PyTuple_CheckExact(op) || !_PyObject_GC_IS_TRACKED(op))
144-
return;
144+
return false;
145145
t = (PyTupleObject *) op;
146146
n = Py_SIZE(t);
147147
for (i = 0; i < n; i++) {
@@ -151,9 +151,17 @@ _PyTuple_MaybeUntrack(PyObject *op)
151151
them yet. */
152152
if (!elt ||
153153
_PyObject_GC_MAY_BE_TRACKED(elt))
154-
return;
154+
return false;
155+
}
156+
return true;
157+
}
158+
159+
void
160+
_PyTuple_MaybeUntrack(PyObject *op)
161+
{
162+
if (_PyTuple_CanUntrack(op)) {
163+
_PyObject_GC_UNTRACK(op);
155164
}
156-
_PyObject_GC_UNTRACK(op);
157165
}
158166

159167
/* Fast, but conservative check if an object maybe tracked

‎Python/gc.c‎

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
#include "pycore_object_alloc.h" // _PyObject_MallocWithType()
1515
#include "pycore_pyerrors.h"
1616
#include "pycore_pystate.h" // _PyThreadState_GET()
17-
#include "pycore_tuple.h" // _PyTuple_MaybeUntrack()
17+
#include "pycore_tuple.h" // _PyTuple_CanUntrack()
1818
#include "pycore_weakref.h" // _PyWeakref_ClearRef()
1919
#include "pydtrace.h"
2020

@@ -595,18 +595,26 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
595595

596596
while (gc != young) {
597597
if (gc_get_refs(gc)) {
598-
/* gc is definitely reachable from outside the
599-
* original 'young'. Mark it as such, and traverse
600-
* its pointers to find any other objects that may
601-
* be directly reachable from it. Note that the
602-
* call to tp_traverse may append objects to young,
603-
* so we have to wait until it returns to determine
604-
* the next object to visit.
605-
*/
598+
// gc is definitely reachable from outside the original 'young'.
599+
// Unless it can be untracked, traverse its pointers to find other
600+
// reachable objects. tp_traverse may append objects to young,
601+
// so wait until it returns to determine the next object to visit.
606602
PyObject *op = FROM_GC(gc);
607-
traverseproc traverse = Py_TYPE(op)->tp_traverse;
608603
_PyObject_ASSERT_WITH_MSG(op, gc_get_refs(gc) > 0,
609604
"refcount is too small");
605+
// An untrackable tuple cannot lead to another GC candidate.
606+
if (PyTuple_CheckExact(op) && _PyTuple_CanUntrack(op)) {
607+
// Ordinary untracking would overwrite the next node's gc_refs.
608+
PyGC_Head *next = GC_NEXT(gc);
609+
_PyGCHead_SET_NEXT(prev, next);
610+
gc->_gc_prev &= _PyGC_PREV_MASK_FINALIZED;
611+
gc->_gc_next = 0;
612+
_PyInterpreterState_GET()->gc.heap_size--;
613+
gc = next;
614+
continue;
615+
}
616+
617+
traverseproc traverse = Py_TYPE(op)->tp_traverse;
610618
// NOTE: visit_reachable may change gc->_gc_next when
611619
// young->_gc_prev == gc. Don't do gc = GC_NEXT(gc) before!
612620
(void) traverse(op,
@@ -616,21 +624,8 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
616624
_PyGCHead_SET_PREV(gc, prev);
617625
// gc is not COLLECTING state after here.
618626
gc_clear_collecting(gc);
619-
if (PyTuple_CheckExact(op)) {
620-
// Untracking gc overwrites the gc_refs of the next object.
621-
PyGC_Head *next = GC_NEXT(gc);
622-
uintptr_t next_refs = next->_gc_prev;
623-
_PyTuple_MaybeUntrack(op);
624-
next->_gc_prev = next_refs;
625-
if (gc->_gc_next != 0) {
626-
prev = gc;
627-
reachable++;
628-
}
629-
}
630-
else {
631-
prev = gc;
632-
reachable++;
633-
}
627+
prev = gc;
628+
reachable++;
634629
}
635630
else {
636631
/* This *may* be unreachable. To make progress,

0 commit comments

Comments
 (0)