Skip to content

Commit 7dbca1b

Browse files
colesburyaisk
authored andcommitted
pythongh-112529: Remove PyGC_Head from object pre-header in free-threaded build (python#114564)
* pythongh-112529: Remove PyGC_Head from object pre-header in free-threaded build This avoids allocating space for PyGC_Head in the free-threaded build. The GC implementation for free-threaded CPython does not use the PyGC_Head structure. * The trashcan mechanism uses the `ob_tid` field instead of `_gc_prev` in the free-threaded build. * The GDB libpython.py file now determines the offset of the managed dict field based on whether the running process is a free-threaded build. Those are identified by the `ob_ref_local` field in PyObject. * Fixes `_PySys_GetSizeOf()` which incorrectly incorrectly included the size of `PyGC_Head` in the size of static `PyTypeObject`.
1 parent 2048885 commit 7dbca1b

File tree

9 files changed

+86
-26
lines changed

9 files changed

+86
-26
lines changed

Include/internal/pycore_object.h

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -315,16 +315,15 @@ static inline void _PyObject_GC_TRACK(
315315
_PyObject_ASSERT_FROM(op, !_PyObject_GC_IS_TRACKED(op),
316316
"object already tracked by the garbage collector",
317317
filename, lineno, __func__);
318-
318+
#ifdef Py_GIL_DISABLED
319+
op->ob_gc_bits |= _PyGC_BITS_TRACKED;
320+
#else
319321
PyGC_Head *gc = _Py_AS_GC(op);
320322
_PyObject_ASSERT_FROM(op,
321323
(gc->_gc_prev & _PyGC_PREV_MASK_COLLECTING) == 0,
322324
"object is in generation which is garbage collected",
323325
filename, lineno, __func__);
324326

325-
#ifdef Py_GIL_DISABLED
326-
op->ob_gc_bits |= _PyGC_BITS_TRACKED;
327-
#else
328327
PyInterpreterState *interp = _PyInterpreterState_GET();
329328
PyGC_Head *generation0 = interp->gc.generation0;
330329
PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev);
@@ -594,8 +593,12 @@ _PyObject_IS_GC(PyObject *obj)
594593
static inline size_t
595594
_PyType_PreHeaderSize(PyTypeObject *tp)
596595
{
597-
return _PyType_IS_GC(tp) * sizeof(PyGC_Head) +
598-
_PyType_HasFeature(tp, Py_TPFLAGS_PREHEADER) * 2 * sizeof(PyObject *);
596+
return (
597+
#ifndef Py_GIL_DISABLED
598+
_PyType_IS_GC(tp) * sizeof(PyGC_Head) +
599+
#endif
600+
_PyType_HasFeature(tp, Py_TPFLAGS_PREHEADER) * 2 * sizeof(PyObject *)
601+
);
599602
}
600603

601604
void _PyObject_GC_Link(PyObject *op);
@@ -625,6 +628,14 @@ extern int _PyObject_StoreInstanceAttribute(PyObject *obj, PyDictValues *values,
625628
PyObject * _PyObject_GetInstanceAttribute(PyObject *obj, PyDictValues *values,
626629
PyObject *name);
627630

631+
#ifdef Py_GIL_DISABLED
632+
# define MANAGED_DICT_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-1)
633+
# define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-2)
634+
#else
635+
# define MANAGED_DICT_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-3)
636+
# define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-4)
637+
#endif
638+
628639
typedef union {
629640
PyObject *dict;
630641
/* Use a char* to generate a warning if directly assigning a PyDictValues */
@@ -635,7 +646,7 @@ static inline PyDictOrValues *
635646
_PyObject_DictOrValuesPointer(PyObject *obj)
636647
{
637648
assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT);
638-
return ((PyDictOrValues *)obj)-3;
649+
return (PyDictOrValues *)((char *)obj + MANAGED_DICT_OFFSET);
639650
}
640651

641652
static inline int
@@ -664,8 +675,6 @@ _PyDictOrValues_SetValues(PyDictOrValues *ptr, PyDictValues *values)
664675
ptr->values = ((char *)values) - 1;
665676
}
666677

667-
#define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-4)
668-
669678
extern PyObject ** _PyObject_ComputedDictPointer(PyObject *);
670679
extern void _PyObject_FreeInstanceAttributes(PyObject *obj);
671680
extern int _PyObject_IsInstanceDictEmpty(PyObject *);

Include/object.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,9 @@ struct _object {
212212
struct _PyMutex { uint8_t v; };
213213

214214
struct _object {
215-
// ob_tid stores the thread id (or zero). It is also used by the GC to
216-
// store linked lists and the computed "gc_refs" refcount.
215+
// ob_tid stores the thread id (or zero). It is also used by the GC and the
216+
// trashcan mechanism as a linked list pointer and by the GC to store the
217+
// computed "gc_refs" refcount.
217218
uintptr_t ob_tid;
218219
uint16_t _padding;
219220
struct _PyMutex ob_mutex; // per-object lock

Lib/test/test_sys.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,6 +1392,7 @@ def setUp(self):
13921392
self.longdigit = sys.int_info.sizeof_digit
13931393
import _testinternalcapi
13941394
self.gc_headsize = _testinternalcapi.SIZEOF_PYGC_HEAD
1395+
self.managed_pre_header_size = _testinternalcapi.SIZEOF_MANAGED_PRE_HEADER
13951396

13961397
check_sizeof = test.support.check_sizeof
13971398

@@ -1427,7 +1428,7 @@ class OverflowSizeof(int):
14271428
def __sizeof__(self):
14281429
return int(self)
14291430
self.assertEqual(sys.getsizeof(OverflowSizeof(sys.maxsize)),
1430-
sys.maxsize + self.gc_headsize*2)
1431+
sys.maxsize + self.gc_headsize + self.managed_pre_header_size)
14311432
with self.assertRaises(OverflowError):
14321433
sys.getsizeof(OverflowSizeof(sys.maxsize + 1))
14331434
with self.assertRaises(ValueError):
@@ -1650,7 +1651,7 @@ def delx(self): del self.__x
16501651
# type
16511652
# static type: PyTypeObject
16521653
fmt = 'P2nPI13Pl4Pn9Pn12PIPc'
1653-
s = vsize('2P' + fmt)
1654+
s = vsize(fmt)
16541655
check(int, s)
16551656
# class
16561657
s = vsize(fmt + # PyTypeObject
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The free-threaded build no longer allocates space for the ``PyGC_Head``
2+
structure in objects that support cyclic garbage collection. A number of
3+
other fields and data structures are used as replacements, including
4+
``ob_gc_bits``, ``ob_tid``, and mimalloc internal data structures.

Modules/_testinternalcapi.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1752,8 +1752,18 @@ module_exec(PyObject *module)
17521752
return 1;
17531753
}
17541754

1755+
Py_ssize_t sizeof_gc_head = 0;
1756+
#ifndef Py_GIL_DISABLED
1757+
sizeof_gc_head = sizeof(PyGC_Head);
1758+
#endif
1759+
17551760
if (PyModule_Add(module, "SIZEOF_PYGC_HEAD",
1756-
PyLong_FromSsize_t(sizeof(PyGC_Head))) < 0) {
1761+
PyLong_FromSsize_t(sizeof_gc_head)) < 0) {
1762+
return 1;
1763+
}
1764+
1765+
if (PyModule_Add(module, "SIZEOF_MANAGED_PRE_HEADER",
1766+
PyLong_FromSsize_t(2 * sizeof(PyObject*))) < 0) {
17571767
return 1;
17581768
}
17591769

Objects/object.c

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2671,7 +2671,12 @@ _PyTrash_thread_deposit_object(struct _py_trashcan *trash, PyObject *op)
26712671
_PyObject_ASSERT(op, _PyObject_IS_GC(op));
26722672
_PyObject_ASSERT(op, !_PyObject_GC_IS_TRACKED(op));
26732673
_PyObject_ASSERT(op, Py_REFCNT(op) == 0);
2674+
#ifdef Py_GIL_DISABLED
2675+
_PyObject_ASSERT(op, op->ob_tid == 0);
2676+
op->ob_tid = (uintptr_t)trash->delete_later;
2677+
#else
26742678
_PyGCHead_SET_PREV(_Py_AS_GC(op), (PyGC_Head*)trash->delete_later);
2679+
#endif
26752680
trash->delete_later = op;
26762681
}
26772682

@@ -2697,8 +2702,12 @@ _PyTrash_thread_destroy_chain(struct _py_trashcan *trash)
26972702
PyObject *op = trash->delete_later;
26982703
destructor dealloc = Py_TYPE(op)->tp_dealloc;
26992704

2700-
trash->delete_later =
2701-
(PyObject*) _PyGCHead_PREV(_Py_AS_GC(op));
2705+
#ifdef Py_GIL_DISABLED
2706+
trash->delete_later = (PyObject*) op->ob_tid;
2707+
op->ob_tid = 0;
2708+
#else
2709+
trash->delete_later = (PyObject*) _PyGCHead_PREV(_Py_AS_GC(op));
2710+
#endif
27022711

27032712
/* Call the deallocator directly. This used to try to
27042713
* fool Py_DECREF into calling it indirectly, but

Python/gc_free_threading.c

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ typedef struct _gc_runtime_state GCState;
2525
// Automatically choose the generation that needs collecting.
2626
#define GENERATION_AUTO (-1)
2727

28-
// A linked-list of objects using the `ob_tid` field as the next pointer.
28+
// A linked list of objects using the `ob_tid` field as the next pointer.
29+
// The linked list pointers are distinct from any real thread ids, because the
30+
// thread ids returned by _Py_ThreadId() are also pointers to distinct objects.
31+
// No thread will confuse its own id with a linked list pointer.
2932
struct worklist {
3033
uintptr_t head;
3134
};
@@ -221,7 +224,7 @@ gc_visit_heaps_lock_held(PyInterpreterState *interp, mi_block_visit_fun *visitor
221224
struct visitor_args *arg)
222225
{
223226
// Offset of PyObject header from start of memory block.
224-
Py_ssize_t offset_base = sizeof(PyGC_Head);
227+
Py_ssize_t offset_base = 0;
225228
if (_PyMem_DebugEnabled()) {
226229
// The debug allocator adds two words at the beginning of each block.
227230
offset_base += 2 * sizeof(size_t);
@@ -331,8 +334,14 @@ update_refs(const mi_heap_t *heap, const mi_heap_area_t *area,
331334
Py_ssize_t refcount = Py_REFCNT(op);
332335
_PyObject_ASSERT(op, refcount >= 0);
333336

334-
// Add the actual refcount to ob_tid.
337+
// We repurpose ob_tid to compute "gc_refs", the number of external
338+
// references to the object (i.e., from outside the GC heaps). This means
339+
// that ob_tid is no longer a valid thread id until it is restored by
340+
// scan_heap_visitor(). Until then, we cannot use the standard reference
341+
// counting functions or allow other threads to run Python code.
335342
gc_maybe_init_refs(op);
343+
344+
// Add the actual refcount to ob_tid.
336345
gc_add_refs(op, refcount);
337346

338347
// Subtract internal references from ob_tid. Objects with ob_tid > 0
@@ -1508,8 +1517,10 @@ gc_alloc(PyTypeObject *tp, size_t basicsize, size_t presize)
15081517
if (mem == NULL) {
15091518
return _PyErr_NoMemory(tstate);
15101519
}
1511-
((PyObject **)mem)[0] = NULL;
1512-
((PyObject **)mem)[1] = NULL;
1520+
if (presize) {
1521+
((PyObject **)mem)[0] = NULL;
1522+
((PyObject **)mem)[1] = NULL;
1523+
}
15131524
PyObject *op = (PyObject *)(mem + presize);
15141525
_PyObject_GC_Link(op);
15151526
return op;

Python/sysmodule.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1878,7 +1878,15 @@ _PySys_GetSizeOf(PyObject *o)
18781878
return (size_t)-1;
18791879
}
18801880

1881-
return (size_t)size + _PyType_PreHeaderSize(Py_TYPE(o));
1881+
size_t presize = 0;
1882+
if (!Py_IS_TYPE(o, &PyType_Type) ||
1883+
PyType_HasFeature((PyTypeObject *)o, Py_TPFLAGS_HEAPTYPE))
1884+
{
1885+
/* Add the size of the pre-header if "o" is not a static type */
1886+
presize = _PyType_PreHeaderSize(Py_TYPE(o));
1887+
}
1888+
1889+
return (size_t)size + presize;
18821890
}
18831891

18841892
static PyObject *

Tools/gdb/libpython.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ def _type_unsigned_int_ptr():
7070
def _sizeof_void_p():
7171
return gdb.lookup_type('void').pointer().sizeof
7272

73+
def _managed_dict_offset():
74+
# See pycore_object.h
75+
pyobj = gdb.lookup_type("PyObject")
76+
if any(field.name == "ob_ref_local" for field in pyobj.fields()):
77+
return -1 * _sizeof_void_p()
78+
else:
79+
return -3 * _sizeof_void_p()
80+
7381

7482
Py_TPFLAGS_MANAGED_DICT = (1 << 4)
7583
Py_TPFLAGS_HEAPTYPE = (1 << 9)
@@ -457,7 +465,7 @@ def get_attr_dict(self):
457465
if dictoffset < 0:
458466
if int_from_int(typeobj.field('tp_flags')) & Py_TPFLAGS_MANAGED_DICT:
459467
assert dictoffset == -1
460-
dictoffset = -3 * _sizeof_void_p()
468+
dictoffset = _managed_dict_offset()
461469
else:
462470
type_PyVarObject_ptr = gdb.lookup_type('PyVarObject').pointer()
463471
tsize = int_from_int(self._gdbval.cast(type_PyVarObject_ptr)['ob_size'])
@@ -485,9 +493,8 @@ def get_keys_values(self):
485493
has_values = int_from_int(typeobj.field('tp_flags')) & Py_TPFLAGS_MANAGED_DICT
486494
if not has_values:
487495
return None
488-
charptrptr_t = _type_char_ptr().pointer()
489-
ptr = self._gdbval.cast(charptrptr_t) - 3
490-
char_ptr = ptr.dereference()
496+
ptr = self._gdbval.cast(_type_char_ptr()) + _managed_dict_offset()
497+
char_ptr = ptr.cast(_type_char_ptr().pointer()).dereference()
491498
if (int(char_ptr) & 1) == 0:
492499
return None
493500
char_ptr += 1

0 commit comments

Comments
 (0)