Skip to content

Commit 402e4d1

Browse files
committed
gh-159437: Support frozen objects in fused GC reference passes
1 parent 6f5db87 commit 402e4d1

7 files changed

Lines changed: 265 additions & 21 deletions

File tree

‎Include/internal/pycore_gc.h‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,15 @@ 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+
#if !defined(Py_GIL_DISABLED) && SIZEOF_VOID_P > 4
122+
// Pointer alignment reserves bit 2 for permanent ownership, including queued weakrefs.
123+
# define _PyGC_PREV_MASK_PERM ((uintptr_t)4)
124+
# define _PyGC_PREV_SHIFT 3
125+
#else
126+
// 32-bit and free-threaded builds retain the existing two-bit header layout.
127+
# define _PyGC_PREV_SHIFT 2
128+
#endif
129+
// The remaining high bits contain the predecessor address or gc_refs.
122130
#define _PyGC_PREV_MASK (((uintptr_t) -1) << _PyGC_PREV_SHIFT)
123131

124132
/* set for debugging information */
@@ -151,7 +159,7 @@ static inline void _PyGCHead_SET_NEXT(PyGC_Head *gc, PyGC_Head *next) {
151159
gc->_gc_next = (uintptr_t)next;
152160
}
153161

154-
// Lowest two bits of _gc_prev is used for _PyGC_PREV_MASK_* flags.
162+
// Low bits of _gc_prev store _PyGC_PREV_MASK_* flags.
155163
static inline PyGC_Head* _PyGCHead_PREV(PyGC_Head *gc) {
156164
uintptr_t prev = (gc->_gc_prev & _PyGC_PREV_MASK);
157165
return (PyGC_Head*)prev;

‎Include/internal/pycore_interp_structs.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ 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.
166+
// Low bits store the flags documented in pycore_gc.h.
167167
// Those bits are made available by the struct's minimum alignment.
168168
uintptr_t _gc_prev;
169169
} PyGC_Head;

‎Include/object.h‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,19 @@ 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+
* For exposed layouts, this must be an integer literal with the value (1 << _PyGC_PREV_SHIFT), in bytes.
116+
* Opaque headers retain 4 for source compatibility.
116117
*/
117-
#define _PyObject_MIN_ALIGNMENT 4
118+
// ABI3T test headers may poison Py_GIL_DISABLED.
119+
#ifndef _Py_OPAQUE_PYOBJECT
120+
# if !defined(Py_GIL_DISABLED) && SIZEOF_VOID_P > 4
121+
# define _PyObject_MIN_ALIGNMENT 8
122+
# else
123+
# define _PyObject_MIN_ALIGNMENT 4
124+
# endif
125+
#else
126+
# define _PyObject_MIN_ALIGNMENT 4
127+
#endif
118128

119129
/* Nothing is actually declared to be a PyObject, but every pointer to
120130
* a Python object can be cast to a PyObject*. This is inheritance built

‎InternalDocs/garbage_collector.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -660,11 +660,15 @@ of `PyGC_Head` discussed in the `Memory layout and object structure`_ section:
660660

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
663-
`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
665-
if an object has been already finalized. During collections `_gc_prev` is
666-
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
663+
`PREV_MASK_COLLECTING` and `_PyGC_PREV_MASK_FINALIZED`. On GIL builds where `SIZEOF_VOID_P > 4`,
664+
bit 2 is also used for `_PyGC_PREV_MASK_PERM` to record permanent-generation
665+
ownership, including weakrefs temporarily queued for callbacks. Unfreeze clears
666+
this flag on objects in the permanent generation. Surviving queued weakrefs
667+
have it cleared before moving to the old generation. Between collections,
668+
`_PyGC_PREV_MASK_FINALIZED` indicates if an object has been already finalized,
669+
and `_PyGC_PREV_MASK_PERM` can also remain set. During collections `_gc_prev`
670+
is temporarily used for storing a copy of the reference count (`gc_ref`), in
671+
addition to these flags, and the GC linked list becomes a singly linked list until
668672
`_gc_prev` is restored.
669673

670674
- The `_gc_next` field is used as the "next" pointer to maintain the doubly linked

‎Lib/test/test_gc.py‎

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,187 @@ def test_freeze(self):
10341034
gc.unfreeze()
10351035
self.assertEqual(gc.get_freeze_count(), 0)
10361036

1037+
@requires_gil_enabled("tests frozen objects in the generation-list collector")
1038+
def test_frozen_references_survive_full_collection(self):
1039+
"""Full collection preserves frozen ownership and reclaims unrelated cycles."""
1040+
gc.collect()
1041+
frozen = []
1042+
gc.freeze()
1043+
self.addCleanup(gc.unfreeze)
1044+
1045+
class Node:
1046+
"""A cycle-capable object whose lifetime can be observed."""
1047+
1048+
owned = Node()
1049+
owned.link = owned
1050+
owned_ref = weakref.ref(owned)
1051+
frozen.append(owned)
1052+
1053+
garbage = Node()
1054+
garbage.link = garbage
1055+
garbage_ref = weakref.ref(garbage)
1056+
del owned, garbage
1057+
1058+
gc.collect(2)
1059+
self.assertIsNotNone(owned_ref())
1060+
self.assertIsNone(garbage_ref())
1061+
1062+
frozen.clear()
1063+
gc.collect(2)
1064+
self.assertIsNone(owned_ref())
1065+
1066+
@requires_gil_enabled("tests repeated permanent-generation transitions")
1067+
@unittest.skipUnless(sys.maxsize > 2**32, "requires 64-bit GC header flags")
1068+
def test_repeated_freeze_and_unfreeze_preserve_cycles(self):
1069+
"""Repeated freezing preserves cycles and unfreezing restores eligibility."""
1070+
import_module("ctypes")
1071+
source = textwrap.dedent("""
1072+
import ctypes
1073+
import gc
1074+
import weakref
1075+
1076+
may_have_foreign_gc_objects = ctypes.pythonapi._PyInterpreterState_MayHaveForeignGcObjects
1077+
may_have_foreign_gc_objects.argtypes = []
1078+
may_have_foreign_gc_objects.restype = ctypes.c_bool
1079+
1080+
class Node:
1081+
"A cycle-capable object whose lifetime can be observed."
1082+
1083+
gc.disable()
1084+
first = Node()
1085+
first.link = first
1086+
first_ref = weakref.ref(first)
1087+
gc.freeze()
1088+
1089+
second = Node()
1090+
second.link = second
1091+
second_ref = weakref.ref(second)
1092+
gc.freeze()
1093+
1094+
# The ordinary holder makes the fused visitor encounter both frozen objects.
1095+
holder = [first, second]
1096+
del first, second
1097+
assert ctypes.sizeof(ctypes.c_void_p) > 4
1098+
assert gc.get_freeze_count() > 0
1099+
assert not may_have_foreign_gc_objects()
1100+
gc.collect(2)
1101+
assert len(holder) == 2
1102+
1103+
gc.unfreeze()
1104+
del holder
1105+
gc.collect(2)
1106+
assert first_ref() is None
1107+
assert second_ref() is None
1108+
""")
1109+
assert_python_ok("-c", source)
1110+
1111+
@requires_gil_enabled("tests permanent weak-reference callback migration")
1112+
@unittest.skipUnless(sys.maxsize > 2**32, "requires 64-bit GC header flags")
1113+
def test_permanent_weakrefs_move_to_old_after_callbacks(self):
1114+
"""Surviving queued weakrefs enter the old generation with their marker cleared."""
1115+
import_module("ctypes")
1116+
source = textwrap.dedent("""
1117+
import ctypes
1118+
import gc
1119+
import sys
1120+
import weakref
1121+
1122+
generation = int(sys.argv[1])
1123+
callbacks = []
1124+
1125+
class Target:
1126+
"A cyclic referent used to queue a permanent weakref."
1127+
1128+
untrack = ctypes.pythonapi.PyObject_GC_UnTrack
1129+
untrack.argtypes = [ctypes.py_object]
1130+
untrack.restype = None
1131+
track = ctypes.pythonapi.PyObject_GC_Track
1132+
track.argtypes = [ctypes.py_object]
1133+
track.restype = None
1134+
1135+
def callback(reference):
1136+
"Retain the callback weakref for post-collection inspection."
1137+
callbacks.append(reference)
1138+
1139+
targets = [Target(), Target()]
1140+
for target in targets:
1141+
target.link = target
1142+
references = [weakref.ref(target, callback) for target in targets]
1143+
gc.freeze()
1144+
1145+
for reference in references:
1146+
previous = ctypes.c_size_t.from_address(id(reference) - ctypes.sizeof(ctypes.c_size_t))
1147+
assert previous.value & 4, hex(previous.value)
1148+
for target in targets:
1149+
untrack(target)
1150+
track(target)
1151+
del target, targets
1152+
1153+
gc.collect(generation)
1154+
1155+
assert len(callbacks) == 2, callbacks
1156+
assert all(reference() is None for reference in references)
1157+
old_generation = min(generation + 1, 2)
1158+
for reference in references:
1159+
previous = ctypes.c_size_t.from_address(id(reference) - ctypes.sizeof(ctypes.c_size_t))
1160+
assert previous.value & 4 == 0, hex(previous.value)
1161+
assert any(obj is reference for obj in gc.get_objects(old_generation))
1162+
1163+
gc.unfreeze()
1164+
callbacks.clear()
1165+
del references
1166+
gc.collect(2)
1167+
""")
1168+
for generation in (0, 2):
1169+
with self.subTest(generation=generation):
1170+
assert_python_ok("-c", source, str(generation))
1171+
1172+
@requires_gil_enabled("tests permanent weak-reference callback ownership")
1173+
@unittest.skipUnless(sys.maxsize > 2**32, "requires 64-bit GC header flags")
1174+
def test_permanent_weakref_callback_can_release_its_last_owner(self):
1175+
"""A permanent weakref can be freed when its callback drops its final owner."""
1176+
import_module("ctypes")
1177+
source = textwrap.dedent("""
1178+
import ctypes
1179+
import gc
1180+
import weakref
1181+
1182+
callbacks = []
1183+
owners = []
1184+
1185+
class Target:
1186+
"A cyclic referent used to queue a permanent weakref."
1187+
1188+
untrack = ctypes.pythonapi.PyObject_GC_UnTrack
1189+
untrack.argtypes = [ctypes.py_object]
1190+
untrack.restype = None
1191+
track = ctypes.pythonapi.PyObject_GC_Track
1192+
track.argtypes = [ctypes.py_object]
1193+
track.restype = None
1194+
1195+
def callback(reference):
1196+
"Release the final owner without retaining the weakref."
1197+
callbacks.append(len(callbacks))
1198+
owners.remove(reference)
1199+
1200+
targets = [Target(), Target()]
1201+
for target in targets:
1202+
target.link = target
1203+
owners.append(weakref.ref(target, callback))
1204+
gc.freeze()
1205+
1206+
for target in targets:
1207+
untrack(target)
1208+
track(target)
1209+
del target, targets
1210+
gc.collect(2)
1211+
1212+
assert callbacks == [0, 1], callbacks
1213+
assert owners == [], owners
1214+
gc.unfreeze()
1215+
""")
1216+
assert_python_ok("-c", source)
1217+
10371218
def test_get_objects(self):
10381219
gc.collect()
10391220
l = []
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow full garbage collections to combine reference passes after :func:`gc.freeze` on GIL builds with pointers larger than four bytes.

‎Python/gc.c‎

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,25 @@ gc_is_collecting(PyGC_Head *g)
6363
return (g->_gc_prev & PREV_MASK_COLLECTING) != 0;
6464
}
6565

66+
#ifdef _PyGC_PREV_MASK_PERM
67+
/* Test whether a GC header retains permanent-generation ownership.
68+
*
69+
* Arguments:
70+
* - `gc`: the object header to inspect.
71+
*
72+
* Return true for permanent objects, including queued weakrefs temporarily
73+
* outside the permanent list.
74+
*/
75+
static inline bool
76+
gc_is_permanent(PyGC_Head *gc)
77+
{
78+
static_assert(_Alignof(PyGC_Head) >= (1 << _PyGC_PREV_SHIFT), "GC headers must satisfy their reserved-bit alignment");
79+
static_assert(_Alignof(PyObject) >= (1 << _PyGC_PREV_SHIFT), "GC objects must satisfy their reserved-bit alignment");
80+
81+
return (gc->_gc_prev & _PyGC_PREV_MASK_PERM) != 0;
82+
}
83+
#endif
84+
6685
static inline void
6786
gc_clear_collecting(PyGC_Head *g)
6887
{
@@ -161,9 +180,10 @@ _gc_prev values
161180
162181
Between collections, _gc_prev is used for doubly linked list.
163182
164-
Lowest two bits of _gc_prev are used for flags.
183+
Lowest bits of _gc_prev are used for flags.
165184
PREV_MASK_COLLECTING is used only while collecting and cleared before GC ends
166185
or _PyObject_GC_UNTRACK() is called.
186+
_PyGC_PREV_MASK_PERM identifies objects owned by the permanent generation.
167187
168188
During a collection, _gc_prev is temporary used for gc_refs, and the gc list
169189
is singly linked until _gc_prev is restored.
@@ -464,9 +484,8 @@ visit_decref(PyObject *op, void *parent)
464484

465485
/* Subtract a GC reference, initializing and marking the referent on first visit.
466486
*
467-
* This visitor is used only when every tracked mortal object it may reach
468-
* belongs to the collection. Non-GC, untracked, and immortal referents remain
469-
* unchanged.
487+
* Every tracked mortal referent must belong to this collection or be marked permanent.
488+
* Non-GC, untracked, immortal, and permanent referents remain unchanged.
470489
*
471490
* Arguments:
472491
* - `op`: the object referenced by the traversed parent.
@@ -490,6 +509,11 @@ gc_decref_and_mark_collecting(PyObject *op, void *parent)
490509
return 0;
491510
}
492511

512+
#ifdef _PyGC_PREV_MASK_PERM
513+
if (gc_is_permanent(gc)) {
514+
return 0;
515+
}
516+
#endif
493517
if (!_PyObject_GC_IS_TRACKED(op) || _Py_IsImmortal(op)) {
494518
return 0;
495519
}
@@ -560,7 +584,7 @@ subtract_refs(PyGC_Head *containers)
560584
* Prerequisites:
561585
* - This is the initial pass of a full collection, not a resurrection recheck.
562586
* - `containers` is a valid list containing all ordinary generations with no masks set.
563-
* - The permanent generation is empty.
587+
* - Permanent objects are identifiable, or the permanent generation is empty.
564588
* - `_PyInterpreterState_MayHaveForeignGcObjects()` is false.
565589
*
566590
* Arguments:
@@ -1086,7 +1110,10 @@ invoke_weakref_callbacks(PyGC_Head *wrcb_to_call, PyGC_Head *old)
10861110
*/
10871111
Py_DECREF(op);
10881112
if (wrcb_to_call->_gc_next == (uintptr_t)gc) {
1089-
/* object is still alive -- move it */
1113+
#ifdef _PyGC_PREV_MASK_PERM
1114+
// The old generation must not retain permanent markers.
1115+
gc->_gc_prev &= ~_PyGC_PREV_MASK_PERM;
1116+
#endif
10901117
gc_list_move(gc, old);
10911118
}
10921119
else {
@@ -1350,11 +1377,13 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable,
13501377
* refcount greater than 0 when all the references within the
13511378
* set are taken into account).
13521379
*/
1353-
// Lazy initialization can touch any tracked mortal referent. Use it only
1354-
// when all such objects belong to this initial full collection.
1380+
// Marked permanent objects are excluded from lazy initialization.
1381+
// Every other tracked mortal referent must belong to this initial full collection.
13551382
Py_ssize_t candidates;
13561383
if (full_collection_pass &&
1384+
#ifndef _PyGC_PREV_MASK_PERM
13571385
gc_list_is_empty(&get_gc_state()->permanent_generation.head) &&
1386+
#endif
13581387
!_PyInterpreterState_MayHaveForeignGcObjects()) {
13591388
candidates = update_and_subtract_refs(base, legacy_finalizer_count_out);
13601389
}
@@ -1986,7 +2015,13 @@ _PyGC_Freeze(PyInterpreterState *interp)
19862015
{
19872016
GCState *gcstate = &interp->gc;
19882017
for (int i = 0; i < NUM_GENERATIONS; ++i) {
1989-
gc_list_merge(GEN_HEAD(gcstate, i), &gcstate->permanent_generation.head);
2018+
PyGC_Head *generation = GEN_HEAD(gcstate, i);
2019+
#ifdef _PyGC_PREV_MASK_PERM
2020+
for (PyGC_Head *gc = GC_NEXT(generation); gc != generation; gc = GC_NEXT(gc)) {
2021+
gc->_gc_prev |= _PyGC_PREV_MASK_PERM;
2022+
}
2023+
#endif
2024+
gc_list_merge(generation, &gcstate->permanent_generation.head);
19902025
gcstate->generations[i].count = 0;
19912026
}
19922027
}
@@ -1995,8 +2030,13 @@ void
19952030
_PyGC_Unfreeze(PyInterpreterState *interp)
19962031
{
19972032
GCState *gcstate = &interp->gc;
1998-
gc_list_merge(&gcstate->permanent_generation.head,
1999-
GEN_HEAD(gcstate, NUM_GENERATIONS-1));
2033+
PyGC_Head *permanent = &gcstate->permanent_generation.head;
2034+
#ifdef _PyGC_PREV_MASK_PERM
2035+
for (PyGC_Head *gc = GC_NEXT(permanent); gc != permanent; gc = GC_NEXT(gc)) {
2036+
gc->_gc_prev &= ~_PyGC_PREV_MASK_PERM;
2037+
}
2038+
#endif
2039+
gc_list_merge(permanent, GEN_HEAD(gcstate, NUM_GENERATIONS-1));
20002040
}
20012041

20022042
Py_ssize_t

0 commit comments

Comments
 (0)