Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Include/internal/pycore_genobject.h
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
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 Python/gc.c
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 Python/gc_free_threading.c
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