Skip to content

Commit 83eb827

Browse files
authored
gh-97922: Run the GC only on eval breaker (#97920)
1 parent c66dbdd commit 83eb827

File tree

8 files changed

+74
-14
lines changed

8 files changed

+74
-14
lines changed

Doc/whatsnew/3.12.rst

+7
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ Other Language Changes
9393
when parsing source code containing null bytes. (Contributed by Pablo Galindo
9494
in :gh:`96670`.)
9595

96+
* The Garbage Collector now runs only on the eval breaker mechanism of the
97+
Python bytecode evaluation loop instead on object allocations. The GC can
98+
also run when :c:func:`PyErr_CheckSignals` is called so C extensions that
99+
need to run for a long time without executing any Python code also have a
100+
chance to execute the GC periodically. (Contributed by Pablo Galindo in
101+
:gh:`97922`.)
102+
96103
New Modules
97104
===========
98105

Include/internal/pycore_gc.h

+2
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ extern void _PyList_ClearFreeList(PyInterpreterState *interp);
202202
extern void _PyDict_ClearFreeList(PyInterpreterState *interp);
203203
extern void _PyAsyncGen_ClearFreeLists(PyInterpreterState *interp);
204204
extern void _PyContext_ClearFreeList(PyInterpreterState *interp);
205+
extern void _Py_ScheduleGC(PyInterpreterState *interp);
206+
extern void _Py_RunGC(PyThreadState *tstate);
205207

206208
#ifdef __cplusplus
207209
}

Include/internal/pycore_interp.h

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ struct _ceval_state {
4949
_Py_atomic_int eval_breaker;
5050
/* Request for dropping the GIL */
5151
_Py_atomic_int gil_drop_request;
52+
/* The GC is ready to be executed */
53+
_Py_atomic_int gc_scheduled;
5254
struct _pending_calls pending;
5355
};
5456

Lib/test/test_frame.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def callback(phase, info):
277277
frame!
278278
"""
279279
nonlocal sneaky_frame_object
280-
sneaky_frame_object = sys._getframe().f_back
280+
sneaky_frame_object = sys._getframe().f_back.f_back
281281
# We're done here:
282282
gc.callbacks.remove(callback)
283283

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The Garbage Collector now runs only on the eval breaker mechanism of the
2+
Python bytecode evaluation loop instead on object allocations. The GC can
3+
also run when :c:func:`PyErr_CheckSignals` is called so C extensions that
4+
need to run for a long time without executing any Python code also have a
5+
chance to execute the GC periodically.

Modules/gcmodule.c

+24-3
Original file line numberDiff line numberDiff line change
@@ -2252,6 +2252,20 @@ PyObject_IS_GC(PyObject *obj)
22522252
return _PyObject_IS_GC(obj);
22532253
}
22542254

2255+
void
2256+
_Py_ScheduleGC(PyInterpreterState *interp)
2257+
{
2258+
GCState *gcstate = &interp->gc;
2259+
if (gcstate->collecting == 1) {
2260+
return;
2261+
}
2262+
struct _ceval_state *ceval = &interp->ceval;
2263+
if (!_Py_atomic_load_relaxed(&ceval->gc_scheduled)) {
2264+
_Py_atomic_store_relaxed(&ceval->gc_scheduled, 1);
2265+
_Py_atomic_store_relaxed(&ceval->eval_breaker, 1);
2266+
}
2267+
}
2268+
22552269
void
22562270
_PyObject_GC_Link(PyObject *op)
22572271
{
@@ -2269,12 +2283,19 @@ _PyObject_GC_Link(PyObject *op)
22692283
!gcstate->collecting &&
22702284
!_PyErr_Occurred(tstate))
22712285
{
2272-
gcstate->collecting = 1;
2273-
gc_collect_generations(tstate);
2274-
gcstate->collecting = 0;
2286+
_Py_ScheduleGC(tstate->interp);
22752287
}
22762288
}
22772289

2290+
void
2291+
_Py_RunGC(PyThreadState *tstate)
2292+
{
2293+
GCState *gcstate = &tstate->interp->gc;
2294+
gcstate->collecting = 1;
2295+
gc_collect_generations(tstate);
2296+
gcstate->collecting = 0;
2297+
}
2298+
22782299
static PyObject *
22792300
gc_alloc(size_t basicsize, size_t presize)
22802301
{

Modules/signalmodule.c

+13
Original file line numberDiff line numberDiff line change
@@ -1798,6 +1798,19 @@ int
17981798
PyErr_CheckSignals(void)
17991799
{
18001800
PyThreadState *tstate = _PyThreadState_GET();
1801+
1802+
/* Opportunistically check if the GC is scheduled to run and run it
1803+
if we have a request. This is done here because native code needs
1804+
to call this API if is going to run for some time without executing
1805+
Python code to ensure signals are handled. Checking for the GC here
1806+
allows long running native code to clean cycles created using the C-API
1807+
even if it doesn't run the evaluation loop */
1808+
struct _ceval_state *interp_ceval_state = &tstate->interp->ceval;
1809+
if (_Py_atomic_load_relaxed(&interp_ceval_state->gc_scheduled)) {
1810+
_Py_atomic_store_relaxed(&interp_ceval_state->gc_scheduled, 0);
1811+
_Py_RunGC(tstate);
1812+
}
1813+
18011814
if (!_Py_ThreadCanHandleSignals(tstate->interp)) {
18021815
return 0;
18031816
}

Python/ceval_gil.c

+20-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "pycore_pyerrors.h" // _PyErr_Fetch()
66
#include "pycore_pylifecycle.h" // _PyErr_Print()
77
#include "pycore_initconfig.h" // _PyStatus_OK()
8+
#include "pycore_interp.h" // _Py_RunGC()
89
#include "pycore_pymem.h" // _PyMem_IsPtrFreed()
910

1011
/*
@@ -69,7 +70,8 @@ COMPUTE_EVAL_BREAKER(PyInterpreterState *interp,
6970
&& _Py_ThreadCanHandleSignals(interp))
7071
| (_Py_atomic_load_relaxed_int32(&ceval2->pending.calls_to_do)
7172
&& _Py_ThreadCanHandlePendingCalls())
72-
| ceval2->pending.async_exc);
73+
| ceval2->pending.async_exc
74+
| _Py_atomic_load_relaxed_int32(&ceval2->gc_scheduled));
7375
}
7476

7577

@@ -938,6 +940,7 @@ _Py_HandlePending(PyThreadState *tstate)
938940
{
939941
_PyRuntimeState * const runtime = &_PyRuntime;
940942
struct _ceval_runtime_state *ceval = &runtime->ceval;
943+
struct _ceval_state *interp_ceval_state = &tstate->interp->ceval;
941944

942945
/* Pending signals */
943946
if (_Py_atomic_load_relaxed_int32(&ceval->signals_pending)) {
@@ -947,20 +950,26 @@ _Py_HandlePending(PyThreadState *tstate)
947950
}
948951

949952
/* Pending calls */
950-
struct _ceval_state *ceval2 = &tstate->interp->ceval;
951-
if (_Py_atomic_load_relaxed_int32(&ceval2->pending.calls_to_do)) {
953+
if (_Py_atomic_load_relaxed_int32(&interp_ceval_state->pending.calls_to_do)) {
952954
if (make_pending_calls(tstate->interp) != 0) {
953955
return -1;
954956
}
955957
}
956958

959+
/* GC scheduled to run */
960+
if (_Py_atomic_load_relaxed_int32(&interp_ceval_state->gc_scheduled)) {
961+
_Py_atomic_store_relaxed(&interp_ceval_state->gc_scheduled, 0);
962+
COMPUTE_EVAL_BREAKER(tstate->interp, ceval, interp_ceval_state);
963+
_Py_RunGC(tstate);
964+
}
965+
957966
/* GIL drop request */
958-
if (_Py_atomic_load_relaxed_int32(&ceval2->gil_drop_request)) {
967+
if (_Py_atomic_load_relaxed_int32(&interp_ceval_state->gil_drop_request)) {
959968
/* Give another thread a chance */
960969
if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) {
961970
Py_FatalError("tstate mix-up");
962971
}
963-
drop_gil(ceval, ceval2, tstate);
972+
drop_gil(ceval, interp_ceval_state, tstate);
964973

965974
/* Other threads may run now */
966975

@@ -981,16 +990,17 @@ _Py_HandlePending(PyThreadState *tstate)
981990
return -1;
982991
}
983992

984-
#ifdef MS_WINDOWS
985-
// bpo-42296: On Windows, _PyEval_SignalReceived() can be called in a
986-
// different thread than the Python thread, in which case
993+
994+
// It is possible that some of the conditions that trigger the eval breaker
995+
// are called in a different thread than the Python thread. An example of
996+
// this is bpo-42296: On Windows, _PyEval_SignalReceived() can be called in
997+
// a different thread than the Python thread, in which case
987998
// _Py_ThreadCanHandleSignals() is wrong. Recompute eval_breaker in the
988999
// current Python thread with the correct _Py_ThreadCanHandleSignals()
9891000
// value. It prevents to interrupt the eval loop at every instruction if
9901001
// the current Python thread cannot handle signals (if
9911002
// _Py_ThreadCanHandleSignals() is false).
992-
COMPUTE_EVAL_BREAKER(tstate->interp, ceval, ceval2);
993-
#endif
1003+
COMPUTE_EVAL_BREAKER(tstate->interp, ceval, interp_ceval_state);
9941004

9951005
return 0;
9961006
}

0 commit comments

Comments
 (0)