Skip to content

Commit fbbf4f4

Browse files
authored
gh-159137: Skip redundant GC resurrection analysis (#159138)
1 parent 8bf6305 commit fbbf4f4

4 files changed

Lines changed: 76 additions & 20 deletions

File tree

‎Include/internal/pycore_genobject.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ extern "C" {
88
# error "this header requires Py_BUILD_CORE define"
99
#endif
1010

11+
#include "pycore_frame.h" // FRAME_STATE_FINISHED()
1112
#include "pycore_interpframe_structs.h" // _PyGenObject
1213

1314
#include <stddef.h> // offsetof()
@@ -21,6 +22,14 @@ PyGenObject *_PyGen_GetGeneratorFromFrame(_PyInterpreterFrame *frame)
2122
return (PyGenObject *)(((char *)frame) - offset_in_gen);
2223
}
2324

25+
// The generator finalizer is a no-op once its frame is cleared.
26+
static inline int
27+
_PyGen_FinalizerIsNoop(PyObject *op)
28+
{
29+
return Py_TYPE(op)->tp_finalize == PyGen_Type.tp_finalize &&
30+
FRAME_STATE_FINISHED(((PyGenObject *)op)->gi_frame_state);
31+
}
32+
2433
PyAPI_FUNC(PyObject *)_PyGen_yf(PyGenObject *);
2534
extern int _PyGen_ClearFrame(PyGenObject *self);
2635

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Avoid a second reachability analysis in the garbage collector when no weakref
2+
callback, finalizer, or collectable-object debug output ran. Finished generators,
3+
coroutines, and async generators can skip this analysis even when finalized.
4+
Free-threaded builds also avoid a second stop-the-world pause in this case.

‎Python/gc.c‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "Python.h"
66
#include "pycore_ceval.h" // _Py_set_eval_breaker_bit()
77
#include "pycore_dict.h" // _PyInlineValuesSize()
8+
#include "pycore_genobject.h" // _PyGen_FinalizerIsNoop()
89
#include "pycore_initconfig.h" // _PyStatus_OK()
910
#include "pycore_context.h"
1011
#include "pycore_interp.h" // PyInterpreterState.gc
@@ -805,7 +806,8 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers)
805806
* Python-level code. See bpo-38006 as an example bug.
806807
*/
807808
static int
808-
handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old)
809+
handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old,
810+
bool *needs_resurrection_check)
809811
{
810812
PyGC_Head *gc;
811813
PyGC_Head wrcb_to_call; /* weakrefs with callbacks to call */
@@ -906,6 +908,9 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old)
906908
/* Invoke the callbacks we decided to honor. It's safe to invoke them
907909
* because they can't reference unreachable objects.
908910
*/
911+
if (!*needs_resurrection_check) {
912+
*needs_resurrection_check = !gc_list_is_empty(&wrcb_to_call);
913+
}
909914
while (! gc_list_is_empty(&wrcb_to_call)) {
910915
PyObject *temp;
911916
PyObject *callback;
@@ -1043,7 +1048,8 @@ handle_legacy_finalizers(PyThreadState *tstate,
10431048
* list, due to refcounts falling to 0.
10441049
*/
10451050
static void
1046-
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
1051+
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable,
1052+
bool *needs_resurrection_check)
10471053
{
10481054
destructor finalize;
10491055
PyGC_Head seen;
@@ -1065,6 +1071,9 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
10651071
if (!_PyGC_FINALIZED(op) &&
10661072
(finalize = Py_TYPE(op)->tp_finalize) != NULL)
10671073
{
1074+
if (!_PyGen_FinalizerIsNoop(op)) {
1075+
*needs_resurrection_check = true;
1076+
}
10681077
_PyGC_SET_FINALIZED(op);
10691078
Py_INCREF(op);
10701079
finalize(op);
@@ -1550,26 +1559,39 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
15501559
validate_list(&finalizers, collecting_clear_unreachable_clear);
15511560
validate_list(&unreachable, collecting_set_unreachable_clear);
15521561

1562+
bool needs_resurrection_check = false;
1563+
15531564
/* Print debugging information. */
15541565
if (gcstate->debug & _PyGC_DEBUG_COLLECTABLE) {
1566+
/* sys.stderr.write() can resurrect objects. */
1567+
needs_resurrection_check = true;
15551568
for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) {
15561569
debug_cycle("collectable", FROM_GC(gc));
15571570
}
15581571
}
15591572

15601573
/* Clear weakrefs and invoke callbacks as necessary. */
1561-
stats.collected += handle_weakref_callbacks(&unreachable, old);
1574+
stats.collected += handle_weakref_callbacks(&unreachable, old,
1575+
&needs_resurrection_check);
15621576
validate_list(old, collecting_clear_unreachable_clear);
15631577
validate_list(&unreachable, collecting_set_unreachable_clear);
15641578

15651579
/* Call tp_finalize on objects which have one. */
1566-
finalize_garbage(tstate, &unreachable);
1580+
finalize_garbage(tstate, &unreachable, &needs_resurrection_check);
15671581

15681582
/* Handle any objects that may have resurrected after the call
15691583
* to 'finalize_garbage' and continue the collection with the
15701584
* objects that are still unreachable */
15711585
PyGC_Head final_unreachable;
1572-
handle_resurrected_objects(&unreachable, &final_unreachable, old);
1586+
if (needs_resurrection_check) {
1587+
handle_resurrected_objects(&unreachable, &final_unreachable, old);
1588+
}
1589+
else {
1590+
/* No Python code ran during finalization, so the original
1591+
* unreachable set is still valid. */
1592+
gc_list_init(&final_unreachable);
1593+
gc_list_merge(&unreachable, &final_unreachable);
1594+
}
15731595

15741596
/* Clear weakrefs to objects in the unreachable set. No Python-level
15751597
* code must be allowed to access those unreachable objects. During

‎Python/gc_free_threading.c‎

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,11 +1542,19 @@ move_legacy_finalizer_reachable(struct collection_state *state)
15421542
// or missing `tp_traverse` methods. When that object goes away, the callback
15431543
// for weakref can be executed and that could reveal unreachable objects to
15441544
// Python-level code. See bpo-38006 as an example bug.
1545-
static void
1546-
find_weakref_callbacks(struct collection_state *state)
1545+
static bool
1546+
prepare_finalization(struct collection_state *state,
1547+
bool *needs_resurrection_check)
15471548
{
1549+
bool has_finalizers = false;
15481550
PyObject *op;
15491551
WORKSTACK_FOR_EACH(&state->unreachable, op) {
1552+
if (!_PyGC_FINALIZED(op) && Py_TYPE(op)->tp_finalize != NULL) {
1553+
has_finalizers = true;
1554+
if (!_PyGen_FinalizerIsNoop(op)) {
1555+
*needs_resurrection_check = true;
1556+
}
1557+
}
15501558
if (!_PyType_SUPPORTS_WEAKREFS(Py_TYPE(op))) {
15511559
continue;
15521560
}
@@ -1590,12 +1598,14 @@ find_weakref_callbacks(struct collection_state *state)
15901598

15911599
// Enqueue weakref to be called later.
15921600
worklist_push(&state->wrcb_to_call, (PyObject *)wr);
1601+
*needs_resurrection_check = true;
15931602
}
15941603
}
1604+
return has_finalizers;
15951605
}
15961606

15971607
// Clear weakrefs to objects in the unreachable set. See comments
1598-
// above find_weakref_callbacks() for why this clearing is required.
1608+
// above prepare_finalization() for why this clearing is required.
15991609
static void
16001610
clear_weakrefs(struct collection_state *state)
16011611
{
@@ -2132,29 +2142,40 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state,
21322142
gc_visit_heaps(interp, &validate_alive_bits, &state->base);
21332143
#endif
21342144

2145+
// Deallocations after the refcount merge may run Python code.
2146+
bool needs_resurrection_check = state->objs_to_decref.head != 0;
2147+
21352148
// Print debugging information.
21362149
if (interp->gc.debug & _PyGC_DEBUG_COLLECTABLE) {
2150+
needs_resurrection_check = true;
21372151
PyObject *op;
21382152
WORKSTACK_FOR_EACH(&state->unreachable, op) {
21392153
debug_cycle("collectable", op);
21402154
}
21412155
}
21422156

2143-
// Find weakref callbacks we will honor (but do not call them).
2144-
find_weakref_callbacks(state);
2145-
_PyEval_StartTheWorld(interp);
2157+
// Find weakref callbacks and pending finalizers (but do not call them).
2158+
bool has_finalizers = prepare_finalization(state, &needs_resurrection_check);
2159+
if (needs_resurrection_check) {
2160+
_PyEval_StartTheWorld(interp);
21462161

2147-
// Deallocate any object from the refcount merge step
2148-
cleanup_worklist(&state->objs_to_decref);
2162+
// Deallocate any object from the refcount merge step
2163+
cleanup_worklist(&state->objs_to_decref);
21492164

2150-
// Call weakref callbacks and finalizers after unpausing other threads to
2151-
// avoid potential deadlocks.
2152-
call_weakref_callbacks(state);
2153-
finalize_garbage(state);
2165+
// Call weakref callbacks and finalizers after unpausing other threads to
2166+
// avoid potential deadlocks.
2167+
call_weakref_callbacks(state);
2168+
finalize_garbage(state);
21542169

2155-
_PyEval_StopTheWorld(interp);
2156-
// Handle any objects that may have resurrected after the finalization.
2157-
err = handle_resurrected_objects(state);
2170+
_PyEval_StopTheWorld(interp);
2171+
// Handle any objects that may have resurrected after the finalization.
2172+
err = handle_resurrected_objects(state);
2173+
}
2174+
else if (has_finalizers) {
2175+
// Only no-op generator finalizers remain; keep other threads paused.
2176+
finalize_garbage(state);
2177+
}
2178+
// Otherwise, keep other threads paused so the unreachable set stays valid.
21582179
// Clear free lists in all threads
21592180
_PyGC_ClearAllFreeLists(interp);
21602181
if (err == 0) {

0 commit comments

Comments
 (0)