diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index 16f876140020bb..e838effc07c5cd 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -62,6 +62,18 @@ _Py_IsMainInterpreter(PyInterpreterState *interp) return (interp == _PyInterpreterState_Main()); } +/* Return whether tracked objects may belong to another interpreter lifetime. + * + * Result: + * - false before any subinterpreter reservation or runtime reinitialization. + * - true after either event, including across Py_Finalize()/Py_Initialize(). + * + * Extension-owned objects may outlive the interpreter whose GC list held them, + * so callers must not assume every tracked object belongs to the current + * runtime when this returns true. + */ +PyAPI_FUNC(bool) _PyInterpreterState_MayHaveForeignGcObjects(void); + extern int _Py_IsMainInterpreterFinalizing(PyInterpreterState *interp); // Export for _interpreters module. diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 919100f2d5ce04..6424fcc5bdd357 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -330,6 +330,7 @@ def f(): self.assertEqual(gc.collect(), 1) def test_saveall(self): + self.addCleanup(gc.collect) # Verify that cyclic garbage like lists show up in gc.garbage if the # SAVEALL option is enabled. @@ -352,6 +353,31 @@ def test_saveall(self): self.assertEqual(len(gc.garbage), 1) obj = gc.garbage.pop() self.assertEqual(id(obj), id_L) + obj.clear() + + # A no-op generator finalizer must still set the finalized marker. + gen = (item for item in ()) + next(gen, None) + self.assertTrue(gc.is_tracked(gen)) + self.assertIsNone(gen.gi_frame) + self.assertFalse(gc.is_finalized(gen)) + L = [gen] + L.append(L) + id_L = id(L) + del gen + + try: + gc.set_debug(gc.DEBUG_SAVEALL) + del L + gc.collect() + saved = next(obj for obj in gc.garbage if id(obj) == id_L) + self.assertTrue(gc.is_finalized(saved[0])) + finally: + gc.set_debug(debug) + for obj in gc.garbage: + if id(obj) == id_L: + obj.clear() + gc.garbage.clear() def test_del(self): # __del__ methods can trigger collection, make this to happen @@ -658,6 +684,83 @@ def __del__(self): lazarus = storage.pop() self.assertTrue(gc.is_finalized(lazarus)) + def test_weakref_callback_installs_finalizer(self): + """A callback can make a finalizer eligible before the object is cleared.""" + self.addCleanup(gc.collect) + events = [] + + class C: + """A self-cycle whose weakref callback installs its finalizer.""" + __slots__ = ("cycle", "__weakref__") + + def finalize(obj): + """Record whether finalization sees the intact cycle.""" + events.append(("finalizer", getattr(obj, "cycle", None) is obj)) + + def callback(reference): + """Install the finalizer after observing the cleared weakref.""" + events.append(("callback", reference() is None)) + C.__del__ = finalize + + obj = C() + obj.cycle = obj + reference = weakref.ref(obj, callback) + gc.collect() + del obj + + self.assertEqual(gc.collect(), 1) + self.assertEqual(events, [("callback", True), ("finalizer", True)]) + self.assertIsNone(reference()) + + @requires_gil_enabled("tests the generation-list collector") + def test_debug_output_resurrects_without_callbacks(self): + """Debug output can resurrect garbage without pending callbacks.""" + was_enabled = gc.isenabled() + gc.disable() + self.addCleanup(gc.enable if was_enabled else gc.disable) + self.addCleanup(gc.collect) + resurrected = [] + + class C: + """An object with a cycle and no finalizer.""" + __slots__ = ("cycle", "__weakref__") + + class Stderr: + """Retain the target when collection reports it as collectable.""" + + def write(self, text): + """Retain the target on collectable debug output. + + text: Debug text written by the collector. + Returns: Number of characters accepted. + """ + if text.startswith("gc: collectable") and not resurrected: + target = reference() + if target is not None: + resurrected.append(target) + return len(text) + + writer = Stderr() + gc.collect() + obj = C() + obj.cycle = obj + reference = weakref.ref(obj) + del obj + + debug = gc.get_debug() + try: + with unittest.mock.patch.object(sys, "stderr", writer): + gc.set_debug(gc.DEBUG_COLLECTABLE) + self.assertEqual(gc.collect(), 0) + self.assertEqual(len(resurrected), 1) + self.assertIs(reference(), resurrected[0]) + self.assertIs(resurrected[0].cycle, resurrected[0]) + finally: + gc.set_debug(debug) + for obj in resurrected: + obj.cycle = None + resurrected.clear() + def test_bug1055820b(self): # Corresponds to temp2b.py in the bug report. @@ -1093,6 +1196,36 @@ def getstats(): gc.enable() + @requires_gil_enabled("tests the generation-list collector") + def test_callback_free_weakref_clear_without_pending_callbacks(self): + """Clear callback-free weakrefs before tp_clear can expose garbage.""" + was_enabled = gc.isenabled() + gc.disable() + self.addCleanup(gc.enable if was_enabled else gc.disable) + self.addCleanup(gc.collect) + observed = [] + self.addCleanup(observed.clear) + + class Observer: + """Inspect a young target when its clearing releases this object.""" + + def __del__(self): + """Record whether the weakref still exposes the target.""" + observed.append(reference()) + + observer = Observer() + gc.collect() + # The older observer does not open the callback path for generation 0. + # Clearing the target releases it, so the weakref must already be dead. + target = C1055820(0) + target.observer = observer + reference = weakref.ref(target) + del observer, target + + self.assertEqual(gc.collect(0), 1) + self.assertEqual(observed, [None]) + self.assertIsNone(reference()) + @unittest.skipIf(ContainerNoGC is None, 'requires ContainerNoGC extension type') def test_trash_weakref_clear(self): @@ -1289,6 +1422,54 @@ def test_heap_size(self): del l self.assertEqual(count, _testinternalcapi.get_tracked_heap_size()) + @requires_gil_enabled("tests fused full-collection reference accounting") + @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") + def test_full_collection_preserves_refs_while_untracking_immortal(self): + """Untracking an immortal preserves adjacent candidates and heap accounting.""" + import_module("ctypes") + source = textwrap.dedent(""" + import ctypes + import gc + import _testinternalcapi + + set_immortal = ctypes.pythonapi._Py_SetImmortal + set_immortal.argtypes = [ctypes.py_object] + set_immortal.restype = None + track = ctypes.pythonapi.PyObject_GC_Track + track.argtypes = [ctypes.py_object] + track.restype = None + may_have_foreign_gc_objects = ctypes.pythonapi._PyInterpreterState_MayHaveForeignGcObjects + may_have_foreign_gc_objects.argtypes = [] + may_have_foreign_gc_objects.restype = ctypes.c_bool + + gc.disable() + gc.collect(2) + candidates_before = gc.get_stats()[2]["candidates"] + parent = [] + middle = [] + set_immortal(middle) + track(middle) + child = [] + parent.append(child) + child.append(parent) + + # Traversing parent initializes child before the intervening + # immortal is removed from the forward chain. + del parent, child + assert gc.get_freeze_count() == 0 + assert not may_have_foreign_gc_objects() + # Every tracked object except middle is a candidate here. + expected_candidates = _testinternalcapi.get_tracked_heap_size() - 1 + assert gc.collect(2) == 2 + assert not gc.is_tracked(middle) + assert gc.get_stats()[2]["candidates"] - candidates_before == expected_candidates + + # get_objects() omits the result list itself. + objects = gc.get_objects() + assert _testinternalcapi.get_tracked_heap_size() == len(objects) + 1 + """) + assert_python_ok("-c", source) + class GCCallbackTests(unittest.TestCase): def setUp(self): diff --git a/Lib/test/test_gdb/test_backtrace.py b/Lib/test/test_gdb/test_backtrace.py index 714853c7b4732d..b975ced3574dd5 100644 --- a/Lib/test/test_gdb/test_backtrace.py +++ b/Lib/test/test_gdb/test_backtrace.py @@ -97,15 +97,21 @@ def test_gc(self): 'def bar():\n' ' foo()\n' 'bar()\n') + # Full GIL collections may skip update_refs(). Stop at a visible phase + # after the collector marks collection active. + if support.Py_GIL_DISABLED: + gc_breakpoint = 'update_refs' + else: + gc_breakpoint = 'prepare_callbacks' # Verify with "py-bt": gdb_output = self.get_stack_trace(cmd, - cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt'], + cmds_after_breakpoint=[f'break {gc_breakpoint}', 'continue', 'py-bt'], ) self.assertIn('Garbage-collecting', gdb_output) # Verify with "py-bt-full": gdb_output = self.get_stack_trace(cmd, - cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt-full'], + cmds_after_breakpoint=[f'break {gc_breakpoint}', 'continue', 'py-bt-full'], ) self.assertIn('Garbage-collecting', gdb_output) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-21-44-05.gh-issue-159437.count1.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-21-44-05.gh-issue-159437.count1.rst new file mode 100644 index 00000000000000..a98b6aee7ef4ff --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-21-44-05.gh-issue-159437.count1.rst @@ -0,0 +1 @@ +Count collectable objects while clearing weak references, avoiding a separate pass during garbage collection. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-23-43-15.gh-issue-159437.LHuMLA.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-23-43-15.gh-issue-159437.LHuMLA.rst new file mode 100644 index 00000000000000..0052c33ea970de --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-23-43-15.gh-issue-159437.LHuMLA.rst @@ -0,0 +1,3 @@ +With the GIL enabled, garbage collection skips the finalization scan when no +finalizers or weakref callbacks are pending and debug output for collectable +objects is disabled. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-00-00-00.gh-issue-159437.weakrf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-00-00-00.gh-issue-159437.weakrf.rst new file mode 100644 index 00000000000000..0dbba4740256a4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-00-00-00.gh-issue-159437.weakrf.rst @@ -0,0 +1 @@ +Garbage collection in builds with the GIL now reuses the initial garbage count and skips late weakref cleanup when no weakrefs need clearing and the unreachable set remains valid. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-01-07-10.gh-issue-159437.reffus.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-01-07-10.gh-issue-159437.reffus.rst new file mode 100644 index 00000000000000..ee5b07d1ce3dac --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-01-07-10.gh-issue-159437.reffus.rst @@ -0,0 +1 @@ +With the GIL enabled, combine reference initialization and subtraction in the initial pass of a full collection when the permanent generation is empty and tracked objects cannot belong to another interpreter lifetime. diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 71bc37194e4b41..c1cf1c1fff0ea6 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -191,6 +191,10 @@ static int test_repeated_init_and_subinterpreters(void) for (int i=1; i <= INIT_LOOPS; i++) { printf("--- Pass %d ---\n", i); _testembed_initialize(); + if (i > 1 && !_PyInterpreterState_MayHaveForeignGcObjects()) { + error("foreign GC object risk was reset"); + return 1; + } mainstate = PyThreadState_Get(); PyEval_ReleaseThread(mainstate); @@ -204,6 +208,10 @@ static int test_repeated_init_and_subinterpreters(void) print_subinterp(); Py_EndInterpreter(substate); } + if (!_PyInterpreterState_MayHaveForeignGcObjects()) { + error("subinterpreter lifetime did not record foreign GC object risk"); + return 1; + } PyThreadState_Swap(mainstate); print_subinterp(); @@ -279,6 +287,14 @@ static int test_repeated_simple_init(void) fflush(stderr); _testembed_initialize(); + if (i == 1 && _PyInterpreterState_MayHaveForeignGcObjects()) { + error("initial main interpreter may have foreign GC objects"); + return 1; + } + if (i > 1 && !_PyInterpreterState_MayHaveForeignGcObjects()) { + error("runtime reinitialization lost foreign GC object risk"); + return 1; + } Py_Finalize(); printf("Finalized\n"); // Give test_embed some output to check } diff --git a/Python/gc.c b/Python/gc.c index 5dc79e20191ebc..77122f62a41481 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -29,9 +29,8 @@ typedef struct _gc_runtime_state GCState; #define GC_NEXT _PyGCHead_NEXT #define GC_PREV _PyGCHead_PREV -// update_refs() set this bit for all objects in current generation. -// subtract_refs() and move_unreachable() uses this to distinguish -// visited object is in GCing or not. +// Reference initialization marks objects in the current generation. +// Reference subtraction and reachability use this bit to recognize them. // // move_unreachable() removes this flag from reachable objects. // Only unreachable objects have this flag. @@ -168,15 +167,14 @@ During a collection, _gc_prev is temporary used for gc_refs, and the gc list is singly linked until _gc_prev is restored. gc_refs - At the start of a collection, update_refs() copies the true refcount - to gc_refs, for each object in the generation being collected. - subtract_refs() then adjusts gc_refs so that it equals the number of - times an object is referenced directly from outside the generation - being collected. + The true refcount is copied to gc_refs and reduced by references from + objects in the generation being collected. The remaining count is the + number of direct references from outside that generation. Full collections + may initialize and subtract these counts in one traversal. PREV_MASK_COLLECTING - Objects in generation being collected are marked PREV_MASK_COLLECTING in - update_refs(). + Reference initialization marks objects in the generation being collected + with PREV_MASK_COLLECTING. _gc_next values @@ -456,6 +454,43 @@ visit_decref(PyObject *op, void *parent) return 0; } +/* Subtract a GC reference, initializing and marking the referent on first visit. + * + * This visitor is used only when every tracked mortal object it may reach + * belongs to the collection. Non-GC, untracked, and immortal referents remain + * unchanged. + * + * Arguments: + * - `op`: the object referenced by the traversed parent. + * - `parent`: the traversed owner used by debug reference validation. + * + * Return zero, as required by traversal callbacks. + */ +static int +gc_decref_and_mark_collecting(PyObject *op, void *parent) +{ + OBJECT_STAT_INC(object_visits); + _PyObject_ASSERT(_PyObject_CAST(parent), !_PyObject_IsFreed(op)); + + if (!_PyObject_IS_GC(op)) { + return 0; + } + + PyGC_Head *gc = AS_GC(op); + if (gc_is_collecting(gc)) { + gc_decref(gc); + return 0; + } + + if (!_PyObject_GC_IS_TRACKED(op) || _Py_IsImmortal(op)) { + return 0; + } + Py_ssize_t refs = Py_REFCNT(op); + _PyObject_ASSERT(op, refs != 0); + gc_reset_refs(gc, refs - 1); + return 0; +} + int _PyGC_VisitStackRef(_PyStackRef *ref, visitproc visit, void *arg) { @@ -463,7 +498,8 @@ _PyGC_VisitStackRef(_PyStackRef *ref, visitproc visit, void *arg) // refcounts when computing the incoming references, but otherwise treat // them like normal. assert(!PyStackRef_IsTaggedInt(*ref)); - if (!PyStackRef_RefcountOnObject(*ref) && (visit == visit_decref)) { + if (!PyStackRef_RefcountOnObject(*ref) && + (visit == visit_decref || visit == gc_decref_and_mark_collecting)) { return 0; } Py_VISIT(PyStackRef_AsPyObjectBorrow(*ref)); @@ -501,6 +537,60 @@ subtract_refs(PyGC_Head *containers) } } +/* Initialize and subtract references in one traversal of all ordinary generations. + * + * Mortal candidates retain adjusted gc_refs. Dynamic immortal candidates become + * untracked without changing other candidates' temporary counts. + * + * Prerequisites: + * - This is the initial pass of a full collection, not a resurrection recheck. + * - `containers` is a valid list containing all ordinary generations with no masks set. + * - The permanent generation is empty. + * - `_PyInterpreterState_MayHaveForeignGcObjects()` is false. + * + * Arguments: + * - `containers`: tracked objects belonging to the initial main interpreter. + * + * Return the number of mortal collection candidates. + */ +static Py_ssize_t +update_and_subtract_refs(PyGC_Head *containers) +{ + Py_ssize_t candidates = 0; + PyGC_Head *previous = containers; + PyGC_Head *gc = GC_NEXT(containers); + + while (gc != containers) { + PyGC_Head *next = GC_NEXT(gc); + PyObject *op = FROM_GC(gc); + + if (!gc_is_collecting(gc)) { + if (_Py_IsImmortal(op)) { + assert(!_Py_IsStaticImmortal(op)); + // The next node may already hold gc_refs instead of a predecessor, + // so ordinary untracking would corrupt its count. + _PyGCHead_SET_NEXT(previous, next); + gc->_gc_prev &= _PyGC_PREV_MASK_FINALIZED; + gc->_gc_next = 0; + _PyInterpreterState_GET()->gc.heap_size--; + gc = next; + continue; + } + gc_reset_refs(gc, Py_REFCNT(op)); + _PyObject_ASSERT(op, gc_get_refs(gc) != 0); + } + + candidates++; + (void)Py_TYPE(op)->tp_traverse(op, gc_decref_and_mark_collecting, op); + previous = gc; + gc = next; + } + + // The tail must remain valid until reachability restores predecessor links. + containers->_gc_prev = (uintptr_t)previous; + return candidates; +} + /* A traversal callback for move_unreachable. */ static int visit_reachable(PyObject *op, void *arg) @@ -756,7 +846,7 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers) } } -/* Handle weakref callbacks. Note that it's possible for such weakrefs to be +/* Prepare weakref callbacks. Note that it's possible for such weakrefs to be * outside the unreachable set -- indeed, those are precisely the weakrefs * whose callbacks must be invoked. See gc_weakref.txt for overview & some * details. @@ -794,17 +884,26 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers) * 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. + * + * Arguments: + * - `unreachable`: normalized collecting list to inspect. + * - `wrcb_to_call`: initialized list receiving weakrefs whose callbacks are safe to invoke. + * - `collectable_count_out`: receives the number of objects in `unreachable`. + * - `needs_weakref_cleanup_out`: receives whether garbage weakrefs or remaining incoming weakrefs may need cleanup. + * + * Return whether any finalizer is pending or a weakref callback is queued. + * This function does not invoke callbacks. The count and cleanup decision + * remain valid only while the unreachable set and its weakrefs cannot change. */ -static int -handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, - bool *needs_resurrection_check) +Py_NO_INLINE static bool +prepare_callbacks(PyGC_Head *unreachable, PyGC_Head *wrcb_to_call, + Py_ssize_t *collectable_count_out, bool *needs_weakref_cleanup_out) { PyGC_Head *gc; - PyGC_Head wrcb_to_call; /* weakrefs with callbacks to call */ PyGC_Head *next; - int num_freed = 0; - - gc_list_init(&wrcb_to_call); + Py_ssize_t count = 0; + bool has_pending_finalizer = false; + bool needs_weakref_cleanup = false; /* Find all weakrefs with callbacks and move into `wrcb_to_call` if the * callback needs to be invoked. We make another pass over wrcb_to_call, @@ -814,9 +913,18 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, PyWeakReference **wrlist; PyObject *op = FROM_GC(gc); + PyTypeObject *type = Py_TYPE(op); + count++; + if (type->tp_finalize != NULL && !has_pending_finalizer) { + has_pending_finalizer = !_PyGC_FINALIZED(op); + } next = GC_NEXT(gc); - if (! _PyType_SUPPORTS_WEAKREFS(Py_TYPE(op))) { + // Garbage weakrefs need clearing even if their referents are outside + // this collection (bpo-38006). + needs_weakref_cleanup |= PyWeakref_Check(op); + + if (! _PyType_SUPPORTS_WEAKREFS(type)) { continue; } @@ -891,21 +999,39 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, PyGC_Head *wrasgc = AS_GC((PyObject *)wr); // wrasgc is reachable, but next isn't, so they can't be the same _PyObject_ASSERT((PyObject *)wr, wrasgc != next); - gc_list_move(wrasgc, &wrcb_to_call); + gc_list_move(wrasgc, wrcb_to_call); } + // Callback-free weakrefs remain linked until finalization. + needs_weakref_cleanup |= *wrlist != NULL; } + *collectable_count_out = count; + *needs_weakref_cleanup_out = needs_weakref_cleanup; + return has_pending_finalizer || !gc_list_is_empty(wrcb_to_call); +} + +/* Invoke queued weakref callbacks and move surviving weakrefs to `old`. + * + * Arguments: + * - `wrcb_to_call`: initialized queue to drain, with a temporary reference to each weakref. + * - `old`: generation receiving weakrefs that survive callback invocation. + * + * Return the number of weakrefs reclaimed while callbacks ran. + */ +static int +invoke_weakref_callbacks(PyGC_Head *wrcb_to_call, PyGC_Head *old) +{ + PyGC_Head *gc; + int num_freed = 0; + /* 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)) { + while (! gc_list_is_empty(wrcb_to_call)) { PyObject *temp; PyObject *callback; - gc = (PyGC_Head*)wrcb_to_call._gc_next; + gc = (PyGC_Head*)wrcb_to_call->_gc_next; PyObject *op = FROM_GC(gc); _PyObject_ASSERT(op, PyWeakref_Check(op)); PyWeakReference *wr = (PyWeakReference *)op; @@ -934,7 +1060,7 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, * ours). */ Py_DECREF(op); - if (wrcb_to_call._gc_next == (uintptr_t)gc) { + if (wrcb_to_call->_gc_next == (uintptr_t)gc) { /* object is still alive -- move it */ gc_list_move(gc, old); } @@ -946,25 +1072,30 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, return num_freed; } -/* Clear all weakrefs to unreachable objects. When this returns, no object in - * `unreachable` is weakly referenced anymore. See the comments above - * handle_weakref_callbacks() for why these weakrefs need to be cleared. +/* Clear all weakrefs to unreachable objects without invoking callbacks. + * When this returns, no object in `unreachable` is weakly referenced anymore. + * See the comments above prepare_callbacks() for why these weakrefs need to be cleared. + * + * `unreachable`: Final collectable list; membership is unchanged. + * Return: Number of objects in `unreachable`. */ -static void +static Py_ssize_t clear_weakrefs(PyGC_Head *unreachable) { PyGC_Head *gc; PyGC_Head *next; + Py_ssize_t count = 0; for (gc = GC_NEXT(unreachable); gc != unreachable; gc = next) { PyWeakReference **wrlist; PyObject *op = FROM_GC(gc); next = GC_NEXT(gc); + count++; if (PyWeakref_Check(op)) { /* A weakref inside the unreachable set is always cleared. See - * the comments above handle_weakref_callbacks() for why these + * the comments above prepare_callbacks() for why these * must be cleared. */ _PyWeakref_ClearRef((PyWeakReference *)op); @@ -994,6 +1125,8 @@ clear_weakrefs(PyGC_Head *unreachable) _PyObject_ASSERT((PyObject *)wr, wr->wr_object == Py_None); } } + + return count; } static void @@ -1036,13 +1169,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 whether a finalizer may have run Python. */ -static void -finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable, - bool *needs_resurrection_check) +static bool +finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable) { destructor finalize; PyGC_Head seen; + bool needs_resurrection_check = false; /* While we're going through the loop, `finalize(op)` may cause op, or * other objects, to be reclaimed via refcounts falling to zero. So @@ -1062,7 +1196,7 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable, (finalize = Py_TYPE(op)->tp_finalize) != NULL) { if (!_PyGen_FinalizerIsNoop(op)) { - *needs_resurrection_check = true; + needs_resurrection_check = true; } _PyGC_SET_FINALIZED(op); Py_INCREF(op); @@ -1072,6 +1206,7 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable, } } gc_list_merge(&seen, collectable); + return needs_resurrection_check; } /* Break reference cycles by clearing the containers involved. This is @@ -1140,6 +1275,8 @@ show_stats_each_generations(GCState *gcstate) /* Deduce which objects among "base" are unreachable from outside the list and move them to 'unreachable'. The process consist in the following steps: +The first two steps may be combined for the initial pass of a full collection. + 1. Copy all reference counts to a different field (gc_prev is used to hold this copy to save memory). 2. Traverse all objects in "base" and visit all referred objects using @@ -1159,6 +1296,13 @@ show_stats_each_generations(GCState *gcstate) * The "unreachable" list must be uninitialized (this function calls gc_list_init over 'unreachable'). + * "full_collection_pass" means this is the initial pass over all ordinary generations. + Resurrection rechecks pass false even during a full collection. + + * "reachable" must point to storage for the number of survivors that remain tracked. + + * The return value is the number of mortal collection candidates. + IMPORTANT: This function leaves 'unreachable' with the NEXT_MASK_UNREACHABLE flag set but it does not clear it to skip unnecessary iteration. Before the flag is cleared (for example, by using 'clear_unreachable_mask' function or @@ -1166,15 +1310,25 @@ 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, - Py_ssize_t *reachable) { + bool full_collection_pass, 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 * refcount greater than 0 when all the references within the * set are taken into account). */ - Py_ssize_t candidates = update_refs(base); // gc_prev is used for gc_refs - subtract_refs(base); + // Lazy initialization can touch any tracked mortal referent. Use it only + // when all such objects belong to this initial full collection. + Py_ssize_t candidates; + if (full_collection_pass && + gc_list_is_empty(&get_gc_state()->permanent_generation.head) && + !_PyInterpreterState_MayHaveForeignGcObjects()) { + candidates = update_and_subtract_refs(base); + } + else { + candidates = update_refs(base); // gc_prev is used for gc_refs + subtract_refs(base); + } /* Leave everything reachable from outside base in base, and move * everything else (in base) to unreachable. @@ -1245,7 +1399,7 @@ handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable, // removed so we can skip the expense of clearing the flag. PyGC_Head* resurrected = unreachable; Py_ssize_t reachable; - deduce_unreachable(resurrected, still_unreachable, &reachable); + deduce_unreachable(resurrected, still_unreachable, false, &reachable); clear_unreachable_mask(still_unreachable); // Move the resurrected objects to the old generation for future collection. @@ -1253,6 +1407,43 @@ handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable, } +/* Invoke weakref callbacks and finalizers, then handle possible resurrection. + * + * Arguments: + * - `tstate`: the thread state used when invoking finalizers. + * - `collectable`: normalized collecting list whose finalizers may run. + * - `still_unreachable`: distinct, uninitialized list head for remaining garbage. + * - `old`: generation receiving surviving weakrefs and resurrected objects. + * - `wrcb_to_call`: initialized, possibly empty list of callback-bearing weakrefs to drain. + * - `collected_count`: count incremented by weakrefs reclaimed while callbacks run. + * - `needs_resurrection_check`: whether earlier work may have run Python. + * + * Return whether resurrection analysis ran. If true, `collectable` must not be + * used again and `still_unreachable` contains the remaining garbage. Otherwise, + * `collectable` remains selected and `still_unreachable` is untouched. + */ +Py_NO_INLINE static bool +handle_callbacks(PyThreadState *tstate, PyGC_Head *collectable, + PyGC_Head *still_unreachable, PyGC_Head *old, + PyGC_Head *wrcb_to_call, Py_ssize_t *collected_count, + bool needs_resurrection_check) +{ + bool weakref_callbacks_invoked = !gc_list_is_empty(wrcb_to_call); + *collected_count += invoke_weakref_callbacks(wrcb_to_call, old); + needs_resurrection_check |= weakref_callbacks_invoked; + validate_list(old, collecting_clear_unreachable_clear); + validate_list(collectable, collecting_set_unreachable_clear); + + // Weakref callbacks can make finalizers newly eligible after the initial + // scan, so we must recheck even when we previously found no pending finalizers. + needs_resurrection_check |= finalize_garbage(tstate, collectable); + if (needs_resurrection_check) { + handle_resurrected_objects(collectable, still_unreachable, old); + } + return needs_resurrection_check; +} + + /* Invoke progress callbacks to notify clients that garbage collection * is starting or stopping */ @@ -1515,7 +1706,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) validate_list(old, collecting_clear_unreachable_clear); Py_ssize_t reachable; - stats.candidates = deduce_unreachable(young, &unreachable, &reachable); + stats.candidates = deduce_unreachable(young, &unreachable, generation == NUM_GENERATIONS - 1, &reachable); /* Move reachable objects to next generation. */ if (young != old) { @@ -1563,28 +1754,26 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) } } - /* Clear weakrefs and invoke callbacks as necessary. */ - 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, &needs_resurrection_check); + PyGC_Head wrcb_to_call; + gc_list_init(&wrcb_to_call); + Py_ssize_t collectable_count; + bool needs_weakref_cleanup; + bool has_pending_callbacks = prepare_callbacks(&unreachable, &wrcb_to_call, &collectable_count, &needs_weakref_cleanup); + has_pending_callbacks |= 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; - 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); + PyGC_Head *collectable = &unreachable; + if (has_pending_callbacks) { + bool resurrection_checked = handle_callbacks(tstate, collectable, &final_unreachable, old, &wrcb_to_call, &stats.collected, needs_resurrection_check); + if (resurrection_checked) { + collectable = &final_unreachable; + // Python code may have changed the garbage set or created weakrefs. + // Refresh both cleanup and counting. + needs_weakref_cleanup = true; + } } + validate_list(old, collecting_clear_unreachable_clear); + validate_list(collectable, collecting_set_unreachable_clear); /* Clear weakrefs to objects in the unreachable set. No Python-level * code must be allowed to access those unreachable objects. During @@ -1592,14 +1781,16 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) * and create new weakrefs. If those weakrefs were not cleared, they * could reveal unreachable objects. Callbacks are not executed. */ - clear_weakrefs(&final_unreachable); + if (needs_weakref_cleanup) { + collectable_count = clear_weakrefs(collectable); + } + stats.collected += collectable_count; - /* Call tp_clear on objects in the final_unreachable set. This will cause + /* Call tp_clear on objects in the collectable 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, old); + delete_garbage(tstate, gcstate, collectable, old); /* Collect statistics on uncollectable objects found and print * debugging information. */ diff --git a/Python/pystate.c b/Python/pystate.c index 97323ea2ba2bce..f55521861d894c 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -309,6 +309,17 @@ _Py_COMP_DIAG_IGNORE_DEPR_DECLS static const _PyRuntimeState initial = _PyRuntimeState_INIT(_PyRuntime, ""); _Py_COMP_DIAG_POP +/* Monotonic guard for tracked objects that may outlive their interpreter. + * Kept outside _PyRuntime so reinitialization cannot clear it. + */ +static int may_have_foreign_gc_objects; + +bool +_PyInterpreterState_MayHaveForeignGcObjects(void) +{ + return _Py_atomic_load_int_relaxed(&may_have_foreign_gc_objects) != 0; +} + #define LOCKS_INIT(runtime) \ { \ &(runtime)->interpreters.mutex, \ @@ -364,6 +375,7 @@ _PyRuntimeState_Init(_PyRuntimeState *runtime) if (runtime->_initialized) { // Py_Initialize() must be running again. + _Py_atomic_store_int_relaxed(&may_have_foreign_gc_objects, 1); // Reset to _PyRuntimeState_INIT. memcpy(runtime, &initial, sizeof(*runtime)); // Preserve the cookie from the original runtime. @@ -723,6 +735,8 @@ _PyInterpreterState_New(PyThreadState *tstate, PyInterpreterState **pinterp) assert(interpreters->main != NULL); assert(id != 0); + _Py_atomic_store_int_relaxed(&may_have_foreign_gc_objects, 1); + interp = alloc_interpreter(); if (interp == NULL) { status = _PyStatus_NO_MEMORY(); diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index db9f74fdbc591f..ba0d4b94286e3f 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -50,6 +50,10 @@ Modules/getbuildinfo.c - initialized - Python/getversion.c - initialized - Python/getversion.c - version - +## monotonic safety guards +# Becomes true once tracked objects may outlive their interpreter. +Python/pystate.c - may_have_foreign_gc_objects - + ## public C-API - set during first init Python/bootstrap_hash.c - _Py_HashSecret_Initialized - Python/pyhash.c - _Py_HashSecret -