GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-159137: Skip redundant GC resurrection analysis by pablogsal · Pull Request #159138 · python/cpython

gh-159137: Skip redundant GC resurrection analysis #159138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Sign up for GitHub

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
pablogsal merged 1 commit into python:main from pablogsal:investigate/gc-pass-fusion-20261010
Oct 10, 2026

Uh oh!

There was an error while loading. Please reload this page.

Merged

gh-159137: Skip redundant GC resurrection analysis #159138

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
1 commit
Select commit
8a5bd9a
gh-159137: Skip redundant GC resurrection analysis
pablogsal Oct 10, 2026
File filter
Filter by extension

Clear filters
Conversations
Failed to load comments.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Jump to
Jump to file
Failed to load files.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Diff view
Diff view

There are no files selected for viewing

9 changes: 9 additions & 0 deletions 9 Include/internal/pycore_genobject.h
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#include "pycore_frame.h" // FRAME_STATE_FINISHED()
#include "pycore_interpframe_structs.h" // _PyGenObject

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

// The generator finalizer is a no-op once its frame is cleared.
static inline int
_PyGen_FinalizerIsNoop(PyObject *op)
{
return Py_TYPE(op)->tp_finalize == PyGen_Type.tp_finalize &&
FRAME_STATE_FINISHED(((PyGenObject *)op)->gi_frame_state);
}

PyAPI_FUNC(PyObject *)_PyGen_yf(PyGenObject *);
extern int _PyGen_ClearFrame(PyGenObject *self);

Expand Down
4 changes: 4 additions & 0 deletions 4 Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-00-00-00.gh-issue-159137.gcReuse.rst
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Avoid a second reachability analysis in the garbage collector when no weakref
callback, finalizer, or collectable-object debug output ran. Finished generators,
coroutines, and async generators can skip this analysis even when finalized.
Free-threaded builds also avoid a second stop-the-world pause in this case.
32 changes: 27 additions & 5 deletions 32 Python/gc.c
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "Python.h"
#include "pycore_ceval.h" // _Py_set_eval_breaker_bit()
#include "pycore_dict.h" // _PyInlineValuesSize()
#include "pycore_genobject.h" // _PyGen_FinalizerIsNoop()
#include "pycore_initconfig.h" // _PyStatus_OK()
#include "pycore_context.h"
#include "pycore_interp.h" // PyInterpreterState.gc
Expand Down Expand Up @@ -805,7 +806,8 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers)
* Python-level code. See bpo-38006 as an example bug.
*/
static int
handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old)
handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old,
bool *needs_resurrection_check)
{
PyGC_Head *gc;
PyGC_Head wrcb_to_call; /* weakrefs with callbacks to call */
Expand Down Expand Up @@ -906,6 +908,9 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old)
/* 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)) {
PyObject *temp;
PyObject *callback;
Expand Down Expand Up @@ -1043,7 +1048,8 @@ handle_legacy_finalizers(PyThreadState *tstate,
* list, due to refcounts falling to 0.
*/
static void
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable,
bool *needs_resurrection_check)
{
destructor finalize;
PyGC_Head seen;
Expand All @@ -1065,6 +1071,9 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
if (!_PyGC_FINALIZED(op) &&
(finalize = Py_TYPE(op)->tp_finalize) != NULL)
{
if (!_PyGen_FinalizerIsNoop(op)) {
*needs_resurrection_check = true;
}
_PyGC_SET_FINALIZED(op);
Py_INCREF(op);
finalize(op);
Expand Down Expand Up @@ -1550,26 +1559,39 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
validate_list(&finalizers, collecting_clear_unreachable_clear);
validate_list(&unreachable, collecting_set_unreachable_clear);

bool needs_resurrection_check = false;

/* Print debugging information. */
if (gcstate->debug & _PyGC_DEBUG_COLLECTABLE) {
/* sys.stderr.write() can resurrect objects. */
needs_resurrection_check = true;
for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) {
debug_cycle("collectable", FROM_GC(gc));
}
}

/* Clear weakrefs and invoke callbacks as necessary. */
stats.collected += handle_weakref_callbacks(&unreachable, old);
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);
finalize_garbage(tstate, &unreachable, &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;
handle_resurrected_objects(&unreachable, &final_unreachable, old);
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);
}

/* Clear weakrefs to objects in the unreachable set. No Python-level
* code must be allowed to access those unreachable objects. During
Expand Down
51 changes: 36 additions & 15 deletions 51 Python/gc_free_threading.c
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1542,11 +1542,19 @@ move_legacy_finalizer_reachable(struct collection_state *state)
// 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.
static void
find_weakref_callbacks(struct collection_state *state)
static bool
prepare_finalization(struct collection_state *state,
bool *needs_resurrection_check)
{
bool has_finalizers = false;
PyObject *op;
WORKSTACK_FOR_EACH(&state->unreachable, op) {
if (!_PyGC_FINALIZED(op) && Py_TYPE(op)->tp_finalize != NULL) {
has_finalizers = true;
if (!_PyGen_FinalizerIsNoop(op)) {
*needs_resurrection_check = true;
}
}
if (!_PyType_SUPPORTS_WEAKREFS(Py_TYPE(op))) {
continue;
}
Expand Down Expand Up @@ -1590,12 +1598,14 @@ find_weakref_callbacks(struct collection_state *state)

// Enqueue weakref to be called later.
worklist_push(&state->wrcb_to_call, (PyObject *)wr);
*needs_resurrection_check = true;
}
}
return has_finalizers;
}

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

// Deallocations after the refcount merge may run Python code.
bool needs_resurrection_check = state->objs_to_decref.head != 0;

// Print debugging information.
if (interp->gc.debug & _PyGC_DEBUG_COLLECTABLE) {
needs_resurrection_check = true;
PyObject *op;
WORKSTACK_FOR_EACH(&state->unreachable, op) {
debug_cycle("collectable", op);
}
}

// Find weakref callbacks we will honor (but do not call them).
find_weakref_callbacks(state);
_PyEval_StartTheWorld(interp);
// Find weakref callbacks and pending finalizers (but do not call them).
bool has_finalizers = prepare_finalization(state, &needs_resurrection_check);
if (needs_resurrection_check) {
_PyEval_StartTheWorld(interp);

// Deallocate any object from the refcount merge step
cleanup_worklist(&state->objs_to_decref);
// Deallocate any object from the refcount merge step
cleanup_worklist(&state->objs_to_decref);

// Call weakref callbacks and finalizers after unpausing other threads to
// avoid potential deadlocks.
call_weakref_callbacks(state);
finalize_garbage(state);
// Call weakref callbacks and finalizers after unpausing other threads to
// avoid potential deadlocks.
call_weakref_callbacks(state);
finalize_garbage(state);

_PyEval_StopTheWorld(interp);
// Handle any objects that may have resurrected after the finalization.
err = handle_resurrected_objects(state);
_PyEval_StopTheWorld(interp);
// Handle any objects that may have resurrected after the finalization.
err = handle_resurrected_objects(state);
}
else if (has_finalizers) {
// Only no-op generator finalizers remain; keep other threads paused.
finalize_garbage(state);
}
// Otherwise, keep other threads paused so the unreachable set stays valid.
// Clear free lists in all threads
_PyGC_ClearAllFreeLists(interp);
if (err == 0) {
Expand Down
Loading
Oops, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.