Skip to content

Commit 7bf0330

Browse files
committed
gh-159137: Restore known-root marking for the GIL garbage collector
1 parent c6c0608 commit 7bf0330

7 files changed

Lines changed: 158 additions & 12 deletions

File tree

‎Include/internal/pycore_gc.h‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,20 @@ static inline void _PyObject_GC_SET_SHARED(PyObject *op) {
118118
/* Bit 1 is set when the object is in generation which is GCed currently. */
119119
#define _PyGC_PREV_MASK_COLLECTING ((uintptr_t)2)
120120

121-
#define _PyGC_PREV_SHIFT 2
121+
/* Use an epoch bit only where the existing header alignment permits it. */
122+
#if !defined(Py_GIL_DISABLED) && SIZEOF_VOID_P == 8
123+
# ifdef __cplusplus
124+
# define _PyGC_PREV_MASK_EPOCH \
125+
((uintptr_t)(alignof(PyGC_Head) >= 8 ? 4 : 0))
126+
# else
127+
# define _PyGC_PREV_MASK_EPOCH \
128+
((uintptr_t)(_Alignof(PyGC_Head) >= 8 ? 4 : 0))
129+
# endif
130+
#else
131+
# define _PyGC_PREV_MASK_EPOCH ((uintptr_t)0)
132+
#endif
133+
134+
#define _PyGC_PREV_SHIFT (2 + (_PyGC_PREV_MASK_EPOCH != 0))
122135
#define _PyGC_PREV_MASK (((uintptr_t) -1) << _PyGC_PREV_SHIFT)
123136

124137
/* set for debugging information */
@@ -151,7 +164,7 @@ static inline void _PyGCHead_SET_NEXT(PyGC_Head *gc, PyGC_Head *next) {
151164
gc->_gc_next = (uintptr_t)next;
152165
}
153166

154-
// Lowest two bits of _gc_prev is used for _PyGC_PREV_MASK_* flags.
167+
// Low bits of _gc_prev are used for _PyGC_PREV_MASK_* flags.
155168
static inline PyGC_Head* _PyGCHead_PREV(PyGC_Head *gc) {
156169
uintptr_t prev = (gc->_gc_prev & _PyGC_PREV_MASK);
157170
return (PyGC_Head*)prev;
@@ -228,6 +241,10 @@ static inline void _PyObject_GC_TRACK(
228241
PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev);
229242
_PyGCHead_SET_NEXT(last, gc);
230243
_PyGCHead_SET_PREV(gc, last);
244+
if (_PyGC_PREV_MASK_EPOCH) {
245+
gc->_gc_prev = (gc->_gc_prev & ~_PyGC_PREV_MASK_EPOCH)
246+
| gcstate->mark_epoch;
247+
}
231248
_PyGCHead_SET_NEXT(gc, generation0);
232249
generation0->_gc_prev = (uintptr_t)gc;
233250
gcstate->heap_size++;

‎Include/internal/pycore_interp_structs.h‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ typedef struct {
163163
_Py_ALIGNED_DEF(_PyObject_MIN_ALIGNMENT, uintptr_t) _gc_next;
164164

165165
// Tagged pointer to previous object in the list.
166-
// Lowest two bits are used for flags documented later.
167-
// Those bits are made available by the struct's minimum alignment.
166+
// Low bits are used for flags documented in pycore_gc.h.
167+
// The available bits depend on the header's existing alignment.
168168
uintptr_t _gc_prev;
169169
} PyGC_Head;
170170

@@ -244,6 +244,10 @@ struct _gc_runtime_state {
244244
#endif
245245
/* true if we are currently running the collector */
246246
int collecting;
247+
#ifndef Py_GIL_DISABLED
248+
/* Epoch bit stamped on tracked objects; toggled by eligible full GCs. */
249+
unsigned char mark_epoch;
250+
#endif
247251
// The frame that started the current collection. It might be NULL even when
248252
// collecting (if no Python frame is running):
249253
_PyInterpreterFrame *frame;

‎Include/object.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ whose size is determined when the object is allocated.
112112

113113
/* PyObjects are given a minimum alignment so that the least significant bits
114114
* of an object pointer become available for other purposes.
115-
* This must be an integer literal with the value (1 << _PyGC_PREV_SHIFT), number of bytes.
115+
* This must be an integer literal specifying the minimum number of bytes.
116116
*/
117117
#define _PyObject_MIN_ALIGNMENT 4
118118

‎InternalDocs/garbage_collector.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,10 +661,10 @@ of `PyGC_Head` discussed in the `Memory layout and object structure`_ section:
661661
- The `_gc_prev` field is normally used as the "previous" pointer to maintain the
662662
doubly linked list but its lowest two bits are used to keep the flags
663663
`PREV_MASK_COLLECTING` and `_PyGC_PREV_MASK_FINALIZED`. Between collections,
664-
the only flag that can be present is `_PyGC_PREV_MASK_FINALIZED` that indicates
664+
the `_PyGC_PREV_MASK_FINALIZED` flag indicates
665665
if an object has been already finalized. During collections `_gc_prev` is
666666
temporarily used for storing a copy of the reference count (`gc_ref`), in
667-
addition to two flags, and the GC linked list becomes a singly linked list until
667+
addition to these flags, and the GC linked list becomes a singly linked list until
668668
`_gc_prev` is restored.
669669

670670
- The `_gc_next` field is used as the "next" pointer to maintain the doubly linked
@@ -676,6 +676,12 @@ of `PyGC_Head` discussed in the `Memory layout and object structure`_ section:
676676
currently in. Instead, when that's needed, ad hoc tricks (like the
677677
`NEXT_MASK_UNREACHABLE` flag) are employed.
678678

679+
On supported 64-bit GIL builds, `_gc_prev` also holds a mark-epoch bit.
680+
Eligible full collections first trace interpreter dictionaries and active frames,
681+
moving known-live objects to a separate list using the existing links. Only the
682+
remaining objects need trial reference counting. This shortcut is disabled for
683+
frozen heaps, secondary interpreters and after interpreter finalization.
684+
679685
Optimization: delayed untracking containers
680686
===========================================
681687

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Restore known-root marking in the GIL garbage collector to reduce trial
2+
reference counting during full collections.

‎Python/gc.c‎

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222

2323
typedef struct _gc_runtime_state GCState;
2424

25+
/* Root marking requires unique interpreter ownership. Keep this state outside
26+
* _PyRuntime so secondary interpreters and runtime reinitialization disable it.
27+
*/
28+
static int gc_ownership_state; // 0: never initialized, 1: unique, 2: unknown
29+
2530
#ifdef Py_DEBUG
2631
# define GC_DEBUG
2732
#endif
@@ -82,10 +87,10 @@ gc_set_refs(PyGC_Head *g, Py_ssize_t refs)
8287
}
8388

8489
static inline void
85-
gc_reset_refs(PyGC_Head *g, Py_ssize_t refs)
90+
gc_reset_refs(PyGC_Head *g, Py_ssize_t refs, uintptr_t epoch)
8691
{
8792
g->_gc_prev = (g->_gc_prev & _PyGC_PREV_MASK_FINALIZED)
88-
| PREV_MASK_COLLECTING
93+
| epoch | PREV_MASK_COLLECTING
8994
| ((uintptr_t)(refs) << _PyGC_PREV_SHIFT);
9095
}
9196

@@ -113,6 +118,10 @@ get_gc_state(void)
113118
void
114119
_PyGC_InitState(GCState *gcstate)
115120
{
121+
int expected = 0;
122+
if (!_Py_atomic_compare_exchange_int(&gc_ownership_state, &expected, 1)) {
123+
_Py_atomic_store_int_relaxed(&gc_ownership_state, 2);
124+
}
116125
#define INIT_HEAD(GEN) \
117126
do { \
118127
GEN.head._gc_next = (uintptr_t)&GEN.head; \
@@ -124,6 +133,7 @@ _PyGC_InitState(GCState *gcstate)
124133
INIT_HEAD(gcstate->generations[i]);
125134
};
126135
gcstate->generation0 = GEN_HEAD(gcstate, 0);
136+
gcstate->mark_epoch = 0;
127137
INIT_HEAD(gcstate->permanent_generation);
128138

129139
#undef INIT_HEAD
@@ -160,7 +170,7 @@ _gc_prev values
160170
161171
Between collections, _gc_prev is used for doubly linked list.
162172
163-
Lowest two bits of _gc_prev are used for flags.
173+
Low bits of _gc_prev are used for flags, including a mark epoch on supported ABIs.
164174
PREV_MASK_COLLECTING is used only while collecting and cleared before GC ends
165175
or _PyObject_GC_UNTRACK() is called.
166176
@@ -400,6 +410,7 @@ update_refs(PyGC_Head *containers)
400410
PyGC_Head *next;
401411
PyGC_Head *gc = GC_NEXT(containers);
402412
Py_ssize_t candidates = 0;
413+
uintptr_t epoch = _PyGC_PREV_MASK_EPOCH ? get_gc_state()->mark_epoch : 0;
403414

404415
while (gc != containers) {
405416
next = GC_NEXT(gc);
@@ -410,7 +421,7 @@ update_refs(PyGC_Head *containers)
410421
gc = next;
411422
continue;
412423
}
413-
gc_reset_refs(gc, Py_REFCNT(op));
424+
gc_reset_refs(gc, Py_REFCNT(op), epoch);
414425
/* Python's cyclic gc should never see an incoming refcount
415426
* of 0: if something decref'ed to 0, it should have been
416427
* deallocated immediately at that time.
@@ -1227,6 +1238,98 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) {
12271238
return candidates;
12281239
}
12291240

1241+
typedef struct {
1242+
PyGC_Head live;
1243+
uintptr_t epoch;
1244+
Py_ssize_t candidates;
1245+
} GCMarkQueue;
1246+
1247+
static int
1248+
visit_add_to_container(PyObject *op, void *arg)
1249+
{
1250+
OBJECT_STAT_INC(object_visits);
1251+
if (!_Py_IsImmortal(op) && _PyObject_IS_GC(op) &&
1252+
_PyObject_GC_IS_TRACKED(op)) {
1253+
PyGC_Head *gc = AS_GC(op);
1254+
GCMarkQueue *queue = arg;
1255+
/* Recheck ownership after tp_is_gc, which can create an interpreter.
1256+
* A partial live set is safe: its outgoing references stay external.
1257+
*/
1258+
if ((gc->_gc_prev & _PyGC_PREV_MASK_EPOCH) != queue->epoch &&
1259+
_Py_atomic_load_int_relaxed(&gc_ownership_state) == 1) {
1260+
gc->_gc_prev = (gc->_gc_prev & ~_PyGC_PREV_MASK_EPOCH)
1261+
| queue->epoch;
1262+
gc_list_move(gc, &queue->live);
1263+
queue->candidates++;
1264+
}
1265+
}
1266+
return 0;
1267+
}
1268+
1269+
/* Avoid extension callbacks while root enumeration holds HEAD_LOCK. */
1270+
static int
1271+
visit_known_root(PyObject *op, void *arg)
1272+
{
1273+
if (op == NULL) {
1274+
return 0;
1275+
}
1276+
if (Py_TYPE(op)->tp_is_gc != NULL) {
1277+
OBJECT_STAT_INC(object_visits);
1278+
return 0;
1279+
}
1280+
return visit_add_to_container(op, arg);
1281+
}
1282+
1283+
static void
1284+
mark_known_roots(GCMarkQueue *queue)
1285+
{
1286+
PyInterpreterState *interp = _PyInterpreterState_GET();
1287+
visit_known_root(interp->sysdict, queue);
1288+
visit_known_root(interp->builtins, queue);
1289+
visit_known_root(interp->dict, queue);
1290+
visit_known_root(interp->imports.modules, queue);
1291+
_Py_FOR_EACH_TSTATE_BEGIN(interp, tstate) {
1292+
for (_PyInterpreterFrame *frame = tstate->current_frame;
1293+
frame != NULL; frame = frame->previous) {
1294+
if (frame->owner >= FRAME_OWNED_BY_INTERPRETER ||
1295+
frame->stackpointer == NULL || _PyFrame_IsIncomplete(frame)) {
1296+
continue;
1297+
}
1298+
(void)_PyFrame_Traverse(frame, visit_known_root, queue);
1299+
}
1300+
}
1301+
_Py_FOR_EACH_TSTATE_END(interp);
1302+
PyGC_Head *gc = GC_NEXT(&queue->live);
1303+
while (gc != &queue->live) {
1304+
PyObject *op = FROM_GC(gc);
1305+
(void)Py_TYPE(op)->tp_traverse(op, visit_add_to_container, queue);
1306+
/* Traversal can append objects, so read next afterward. */
1307+
gc = GC_NEXT(gc);
1308+
}
1309+
}
1310+
1311+
/* Toggle the epoch to distinguish unseen objects. GC_TRACK stamps new objects
1312+
* with the new epoch. The live list doubles as the pending work queue.
1313+
*/
1314+
static Py_ssize_t
1315+
collect_from_known_roots(PyGC_Head *base, PyGC_Head *unreachable)
1316+
{
1317+
GCState *gcstate = get_gc_state();
1318+
gcstate->mark_epoch ^= _PyGC_PREV_MASK_EPOCH;
1319+
GCMarkQueue queue = {.epoch = gcstate->mark_epoch};
1320+
gc_list_init(&queue.live);
1321+
mark_known_roots(&queue);
1322+
1323+
/* Known-live references stay external to the residual analysis.
1324+
* update_refs stamps the remaining candidates before callbacks can run.
1325+
*/
1326+
Py_ssize_t candidates = deduce_unreachable(base, unreachable);
1327+
gc_list_merge(base, &queue.live);
1328+
gc_list_merge(&queue.live, base);
1329+
validate_list(base, collecting_clear_unreachable_clear);
1330+
return queue.candidates + candidates;
1331+
}
1332+
12301333
/* Handle objects that may have resurrected after a call to 'finalize_garbage', moving
12311334
them to 'old_generation' and placing the rest on 'still_unreachable'.
12321335
@@ -1521,7 +1624,19 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
15211624
}
15221625
validate_list(old, collecting_clear_unreachable_clear);
15231626

1524-
stats.candidates = deduce_unreachable(young, &unreachable);
1627+
if (_PyGC_PREV_MASK_EPOCH && generation == NUM_GENERATIONS - 1 &&
1628+
_Py_IsMainInterpreter(tstate->interp) &&
1629+
gcstate->heap_size >= 4096 &&
1630+
gc_list_is_empty(&gcstate->permanent_generation.head) &&
1631+
_Py_atomic_load_int_relaxed(&gc_ownership_state) == 1) {
1632+
/* Without frozen or foreign objects, every tracked non-immortal
1633+
* object belongs to this full collection before update_refs.
1634+
*/
1635+
stats.candidates = collect_from_known_roots(young, &unreachable);
1636+
}
1637+
else {
1638+
stats.candidates = deduce_unreachable(young, &unreachable);
1639+
}
15251640

15261641
untrack_tuples(young);
15271642
/* Move reachable objects to next generation. */
@@ -1901,6 +2016,7 @@ finalize_unlink_gc_head(PyGC_Head *gc) {
19012016
void
19022017
_PyGC_Fini(PyInterpreterState *interp)
19032018
{
2019+
_Py_atomic_store_int_relaxed(&gc_ownership_state, 2);
19042020
GCState *gcstate = &interp->gc;
19052021
Py_CLEAR(gcstate->garbage);
19062022
Py_CLEAR(gcstate->callbacks);

‎Tools/c-analyzer/cpython/ignored.tsv‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Python/fileutils.c set_inheritable ioctl_works -
2222
# (set lazily, atomically, *after* first init)
2323
Modules/posixmodule.c os_dup2_impl dup3_works_atomic -
2424
Modules/posixmodule.c os_pipe_impl pipe2_works_atomic -
25+
Python/gc.c - gc_ownership_state Atomic process-lifetime ownership guard; never reset across interpreter or runtime finalization.
2526

2627
## guards around resource init
2728
Python/thread_pthread.h PyThread__init_thread lib_initialized -

0 commit comments

Comments
 (0)