Skip to content

Commit abd738f

Browse files
committed
gh-159437: Skip unnecessary GC finalizer scans
1 parent 75ce53d commit abd738f

3 files changed

Lines changed: 204 additions & 45 deletions

File tree

‎Lib/test/test_gc.py‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ def f():
330330
self.assertEqual(gc.collect(), 1)
331331

332332
def test_saveall(self):
333+
self.addCleanup(gc.collect)
333334
# Verify that cyclic garbage like lists show up in gc.garbage if the
334335
# SAVEALL option is enabled.
335336

@@ -352,6 +353,31 @@ def test_saveall(self):
352353
self.assertEqual(len(gc.garbage), 1)
353354
obj = gc.garbage.pop()
354355
self.assertEqual(id(obj), id_L)
356+
obj.clear()
357+
358+
# A no-op generator finalizer must still set the finalized marker.
359+
gen = (item for item in ())
360+
next(gen, None)
361+
self.assertTrue(gc.is_tracked(gen))
362+
self.assertIsNone(gen.gi_frame)
363+
self.assertFalse(gc.is_finalized(gen))
364+
L = [gen]
365+
L.append(L)
366+
id_L = id(L)
367+
del gen
368+
369+
try:
370+
gc.set_debug(gc.DEBUG_SAVEALL)
371+
del L
372+
gc.collect()
373+
saved = next(obj for obj in gc.garbage if id(obj) == id_L)
374+
self.assertTrue(gc.is_finalized(saved[0]))
375+
finally:
376+
gc.set_debug(debug)
377+
for obj in gc.garbage:
378+
if id(obj) == id_L:
379+
obj.clear()
380+
gc.garbage.clear()
355381

356382
def test_del(self):
357383
# __del__ methods can trigger collection, make this to happen
@@ -658,6 +684,83 @@ def __del__(self):
658684
lazarus = storage.pop()
659685
self.assertTrue(gc.is_finalized(lazarus))
660686

687+
def test_weakref_callback_installs_finalizer(self):
688+
"""A callback can make a finalizer eligible before the object is cleared."""
689+
self.addCleanup(gc.collect)
690+
events = []
691+
692+
class C:
693+
"""A self-cycle whose weakref callback installs its finalizer."""
694+
__slots__ = ("cycle", "__weakref__")
695+
696+
def finalize(obj):
697+
"""Record whether finalization sees the intact cycle."""
698+
events.append(("finalizer", getattr(obj, "cycle", None) is obj))
699+
700+
def callback(reference):
701+
"""Install the finalizer after observing the cleared weakref."""
702+
events.append(("callback", reference() is None))
703+
C.__del__ = finalize
704+
705+
obj = C()
706+
obj.cycle = obj
707+
reference = weakref.ref(obj, callback)
708+
gc.collect()
709+
del obj
710+
711+
self.assertEqual(gc.collect(), 1)
712+
self.assertEqual(events, [("callback", True), ("finalizer", True)])
713+
self.assertIsNone(reference())
714+
715+
@requires_gil_enabled("tests the generation-list collector")
716+
def test_debug_output_resurrects_without_callbacks(self):
717+
"""Debug output can resurrect garbage without pending callbacks."""
718+
was_enabled = gc.isenabled()
719+
gc.disable()
720+
self.addCleanup(gc.enable if was_enabled else gc.disable)
721+
self.addCleanup(gc.collect)
722+
resurrected = []
723+
724+
class C:
725+
"""An object with a cycle and no finalizer."""
726+
__slots__ = ("cycle", "__weakref__")
727+
728+
class Stderr:
729+
"""Retain the target when collection reports it as collectable."""
730+
731+
def write(self, text):
732+
"""Retain the target on collectable debug output.
733+
734+
text: Debug text written by the collector.
735+
Returns: Number of characters accepted.
736+
"""
737+
if text.startswith("gc: collectable") and not resurrected:
738+
target = reference()
739+
if target is not None:
740+
resurrected.append(target)
741+
return len(text)
742+
743+
writer = Stderr()
744+
gc.collect()
745+
obj = C()
746+
obj.cycle = obj
747+
reference = weakref.ref(obj)
748+
del obj
749+
750+
debug = gc.get_debug()
751+
try:
752+
with unittest.mock.patch.object(sys, "stderr", writer):
753+
gc.set_debug(gc.DEBUG_COLLECTABLE)
754+
self.assertEqual(gc.collect(), 0)
755+
self.assertEqual(len(resurrected), 1)
756+
self.assertIs(reference(), resurrected[0])
757+
self.assertIs(resurrected[0].cycle, resurrected[0])
758+
finally:
759+
gc.set_debug(debug)
760+
for obj in resurrected:
761+
obj.cycle = None
762+
resurrected.clear()
763+
661764
def test_bug1055820b(self):
662765
# Corresponds to temp2b.py in the bug report.
663766

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
With the GIL enabled, garbage collection skips the finalization scan when no
2+
finalizers or weakref callbacks are pending and debug output for collectable
3+
objects is disabled.

‎Python/gc.c‎

Lines changed: 98 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers)
756756
}
757757
}
758758

759-
/* Handle weakref callbacks. Note that it's possible for such weakrefs to be
759+
/* Prepare weakref callbacks. Note that it's possible for such weakrefs to be
760760
* outside the unreachable set -- indeed, those are precisely the weakrefs
761761
* whose callbacks must be invoked. See gc_weakref.txt for overview & some
762762
* details.
@@ -794,17 +794,20 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers)
794794
* or missing `tp_traverse` methods. When that object goes away, the callback
795795
* for weakref can be executed and that could reveal unreachable objects to
796796
* Python-level code. See bpo-38006 as an example bug.
797+
*
798+
* Arguments:
799+
* - `unreachable`: normalized collecting list to inspect.
800+
* - `wrcb_to_call`: initialized list receiving weakrefs whose callbacks are safe to invoke.
801+
*
802+
* Return whether any finalizer is pending or a weakref callback is queued.
803+
* This function does not invoke callbacks.
797804
*/
798-
static int
799-
handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old,
800-
bool *needs_resurrection_check)
805+
Py_NO_INLINE static bool
806+
prepare_callbacks(PyGC_Head *unreachable, PyGC_Head *wrcb_to_call)
801807
{
802808
PyGC_Head *gc;
803-
PyGC_Head wrcb_to_call; /* weakrefs with callbacks to call */
804809
PyGC_Head *next;
805-
int num_freed = 0;
806-
807-
gc_list_init(&wrcb_to_call);
810+
bool has_pending_finalizer = false;
808811

809812
/* Find all weakrefs with callbacks and move into `wrcb_to_call` if the
810813
* callback needs to be invoked. We make another pass over wrcb_to_call,
@@ -814,9 +817,13 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old,
814817
PyWeakReference **wrlist;
815818

816819
PyObject *op = FROM_GC(gc);
820+
PyTypeObject *type = Py_TYPE(op);
821+
if (type->tp_finalize != NULL && !has_pending_finalizer) {
822+
has_pending_finalizer = !_PyGC_FINALIZED(op);
823+
}
817824
next = GC_NEXT(gc);
818825

819-
if (! _PyType_SUPPORTS_WEAKREFS(Py_TYPE(op))) {
826+
if (! _PyType_SUPPORTS_WEAKREFS(type)) {
820827
continue;
821828
}
822829

@@ -891,21 +898,35 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old,
891898
PyGC_Head *wrasgc = AS_GC((PyObject *)wr);
892899
// wrasgc is reachable, but next isn't, so they can't be the same
893900
_PyObject_ASSERT((PyObject *)wr, wrasgc != next);
894-
gc_list_move(wrasgc, &wrcb_to_call);
901+
gc_list_move(wrasgc, wrcb_to_call);
895902
}
896903
}
897904

905+
return has_pending_finalizer || !gc_list_is_empty(wrcb_to_call);
906+
}
907+
908+
/* Invoke queued weakref callbacks and move surviving weakrefs to `old`.
909+
*
910+
* Arguments:
911+
* - `wrcb_to_call`: initialized queue to drain, with a temporary reference to each weakref.
912+
* - `old`: generation receiving weakrefs that survive callback invocation.
913+
*
914+
* Return the number of weakrefs reclaimed while callbacks ran.
915+
*/
916+
static int
917+
invoke_weakref_callbacks(PyGC_Head *wrcb_to_call, PyGC_Head *old)
918+
{
919+
PyGC_Head *gc;
920+
int num_freed = 0;
921+
898922
/* Invoke the callbacks we decided to honor. It's safe to invoke them
899923
* because they can't reference unreachable objects.
900924
*/
901-
if (!*needs_resurrection_check) {
902-
*needs_resurrection_check = !gc_list_is_empty(&wrcb_to_call);
903-
}
904-
while (! gc_list_is_empty(&wrcb_to_call)) {
925+
while (! gc_list_is_empty(wrcb_to_call)) {
905926
PyObject *temp;
906927
PyObject *callback;
907928

908-
gc = (PyGC_Head*)wrcb_to_call._gc_next;
929+
gc = (PyGC_Head*)wrcb_to_call->_gc_next;
909930
PyObject *op = FROM_GC(gc);
910931
_PyObject_ASSERT(op, PyWeakref_Check(op));
911932
PyWeakReference *wr = (PyWeakReference *)op;
@@ -934,7 +955,7 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old,
934955
* ours).
935956
*/
936957
Py_DECREF(op);
937-
if (wrcb_to_call._gc_next == (uintptr_t)gc) {
958+
if (wrcb_to_call->_gc_next == (uintptr_t)gc) {
938959
/* object is still alive -- move it */
939960
gc_list_move(gc, old);
940961
}
@@ -948,7 +969,7 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old,
948969

949970
/* Clear all weakrefs to unreachable objects. When this returns, no object in
950971
* `unreachable` is weakly referenced anymore. See the comments above
951-
* handle_weakref_callbacks() for why these weakrefs need to be cleared.
972+
* prepare_callbacks() for why these weakrefs need to be cleared.
952973
*/
953974
static void
954975
clear_weakrefs(PyGC_Head *unreachable)
@@ -964,7 +985,7 @@ clear_weakrefs(PyGC_Head *unreachable)
964985

965986
if (PyWeakref_Check(op)) {
966987
/* A weakref inside the unreachable set is always cleared. See
967-
* the comments above handle_weakref_callbacks() for why these
988+
* the comments above prepare_callbacks() for why these
968989
* must be cleared.
969990
*/
970991
_PyWeakref_ClearRef((PyWeakReference *)op);
@@ -1036,13 +1057,14 @@ handle_legacy_finalizers(PyThreadState *tstate,
10361057
/* Run first-time finalizers (if any) on all the objects in collectable.
10371058
* Note that this may remove some (or even all) of the objects from the
10381059
* list, due to refcounts falling to 0.
1060+
* Return whether a finalizer may have run Python.
10391061
*/
1040-
static void
1041-
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable,
1042-
bool *needs_resurrection_check)
1062+
static bool
1063+
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
10431064
{
10441065
destructor finalize;
10451066
PyGC_Head seen;
1067+
bool needs_resurrection_check = false;
10461068

10471069
/* While we're going through the loop, `finalize(op)` may cause op, or
10481070
* other objects, to be reclaimed via refcounts falling to zero. So
@@ -1062,7 +1084,7 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable,
10621084
(finalize = Py_TYPE(op)->tp_finalize) != NULL)
10631085
{
10641086
if (!_PyGen_FinalizerIsNoop(op)) {
1065-
*needs_resurrection_check = true;
1087+
needs_resurrection_check = true;
10661088
}
10671089
_PyGC_SET_FINALIZED(op);
10681090
Py_INCREF(op);
@@ -1072,6 +1094,7 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable,
10721094
}
10731095
}
10741096
gc_list_merge(&seen, collectable);
1097+
return needs_resurrection_check;
10751098
}
10761099

10771100
/* Break reference cycles by clearing the containers involved. This is
@@ -1253,6 +1276,43 @@ handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable,
12531276
}
12541277

12551278

1279+
/* Invoke weakref callbacks and finalizers, then handle possible resurrection.
1280+
*
1281+
* Arguments:
1282+
* - `tstate`: the thread state used when invoking finalizers.
1283+
* - `collectable`: normalized collecting list whose finalizers may run.
1284+
* - `still_unreachable`: distinct, uninitialized list head for remaining garbage.
1285+
* - `old`: generation receiving surviving weakrefs and resurrected objects.
1286+
* - `wrcb_to_call`: initialized, possibly empty list of callback-bearing weakrefs to drain.
1287+
* - `collected_count`: count incremented by weakrefs reclaimed while callbacks run.
1288+
* - `needs_resurrection_check`: whether earlier work may have run Python.
1289+
*
1290+
* Return whether resurrection analysis ran. If true, `collectable` must not be
1291+
* used again and `still_unreachable` contains the remaining garbage. Otherwise,
1292+
* `collectable` remains selected and `still_unreachable` is untouched.
1293+
*/
1294+
Py_NO_INLINE static bool
1295+
handle_callbacks(PyThreadState *tstate, PyGC_Head *collectable,
1296+
PyGC_Head *still_unreachable, PyGC_Head *old,
1297+
PyGC_Head *wrcb_to_call, Py_ssize_t *collected_count,
1298+
bool needs_resurrection_check)
1299+
{
1300+
bool weakref_callbacks_invoked = !gc_list_is_empty(wrcb_to_call);
1301+
*collected_count += invoke_weakref_callbacks(wrcb_to_call, old);
1302+
needs_resurrection_check |= weakref_callbacks_invoked;
1303+
validate_list(old, collecting_clear_unreachable_clear);
1304+
validate_list(collectable, collecting_set_unreachable_clear);
1305+
1306+
// Weakref callbacks can make finalizers newly eligible after the initial
1307+
// scan, so we must recheck even when we previously found no pending finalizers.
1308+
needs_resurrection_check |= finalize_garbage(tstate, collectable);
1309+
if (needs_resurrection_check) {
1310+
handle_resurrected_objects(collectable, still_unreachable, old);
1311+
}
1312+
return needs_resurrection_check;
1313+
}
1314+
1315+
12561316
/* Invoke progress callbacks to notify clients that garbage collection
12571317
* is starting or stopping
12581318
*/
@@ -1563,43 +1623,36 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
15631623
}
15641624
}
15651625

1566-
/* Clear weakrefs and invoke callbacks as necessary. */
1567-
stats.collected += handle_weakref_callbacks(&unreachable, old,
1568-
&needs_resurrection_check);
1569-
validate_list(old, collecting_clear_unreachable_clear);
1570-
validate_list(&unreachable, collecting_set_unreachable_clear);
1571-
1572-
/* Call tp_finalize on objects which have one. */
1573-
finalize_garbage(tstate, &unreachable, &needs_resurrection_check);
1626+
PyGC_Head wrcb_to_call;
1627+
gc_list_init(&wrcb_to_call);
1628+
bool has_pending_callbacks = prepare_callbacks(&unreachable, &wrcb_to_call);
1629+
has_pending_callbacks |= needs_resurrection_check;
15741630

1575-
/* Handle any objects that may have resurrected after the call
1576-
* to 'finalize_garbage' and continue the collection with the
1577-
* objects that are still unreachable */
15781631
PyGC_Head final_unreachable;
1579-
if (needs_resurrection_check) {
1580-
handle_resurrected_objects(&unreachable, &final_unreachable, old);
1581-
}
1582-
else {
1583-
/* No Python code ran during finalization, so the original
1584-
* unreachable set is still valid. */
1585-
gc_list_init(&final_unreachable);
1586-
gc_list_merge(&unreachable, &final_unreachable);
1632+
PyGC_Head *collectable = &unreachable;
1633+
if (has_pending_callbacks) {
1634+
bool resurrection_checked = handle_callbacks(tstate, collectable, &final_unreachable, old, &wrcb_to_call, &stats.collected, needs_resurrection_check);
1635+
if (resurrection_checked) {
1636+
collectable = &final_unreachable;
1637+
}
15871638
}
1639+
validate_list(old, collecting_clear_unreachable_clear);
1640+
validate_list(collectable, collecting_set_unreachable_clear);
15881641

15891642
/* Clear weakrefs to objects in the unreachable set. No Python-level
15901643
* code must be allowed to access those unreachable objects. During
15911644
* delete_garbage(), finalizers outside the unreachable set might run
15921645
* and create new weakrefs. If those weakrefs were not cleared, they
15931646
* could reveal unreachable objects. Callbacks are not executed.
15941647
*/
1595-
clear_weakrefs(&final_unreachable);
1648+
clear_weakrefs(collectable);
15961649

1597-
/* Call tp_clear on objects in the final_unreachable set. This will cause
1650+
/* Call tp_clear on objects in the collectable set. This will cause
15981651
* the reference cycles to be broken. It may also cause some objects
15991652
* in finalizers to be freed.
16001653
*/
1601-
stats.collected += gc_list_size(&final_unreachable);
1602-
delete_garbage(tstate, gcstate, &final_unreachable, old);
1654+
stats.collected += gc_list_size(collectable);
1655+
delete_garbage(tstate, gcstate, collectable, old);
16031656

16041657
/* Collect statistics on uncollectable objects found and print
16051658
* debugging information. */

0 commit comments

Comments
 (0)