Skip to content

Commit bc262de

Browse files
authored
GH-125174: Mark objects as statically allocated. (#127797)
* Set a bit in the unused part of the refcount on 64 bit machines and the free-threaded build. * Use the top of the refcount range on 32 bit machines
1 parent dd9da73 commit bc262de

File tree

7 files changed

+99
-13
lines changed

7 files changed

+99
-13
lines changed

Include/internal/pycore_object.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,24 @@ PyAPI_FUNC(int) _PyObject_IsFreed(PyObject *);
7373
#define _PyObject_HEAD_INIT(type) \
7474
{ \
7575
.ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL, \
76+
.ob_flags = _Py_STATICALLY_ALLOCATED_FLAG, \
7677
.ob_type = (type) \
7778
}
7879
#else
80+
#if SIZEOF_VOID_P > 4
7981
#define _PyObject_HEAD_INIT(type) \
8082
{ \
81-
.ob_refcnt = _Py_IMMORTAL_INITIAL_REFCNT, \
83+
.ob_refcnt = _Py_IMMORTAL_INITIAL_REFCNT, \
84+
.ob_flags = _Py_STATICALLY_ALLOCATED_FLAG, \
8285
.ob_type = (type) \
8386
}
87+
#else
88+
#define _PyObject_HEAD_INIT(type) \
89+
{ \
90+
.ob_refcnt = _Py_STATIC_IMMORTAL_INITIAL_REFCNT, \
91+
.ob_type = (type) \
92+
}
93+
#endif
8494
#endif
8595
#define _PyVarObject_HEAD_INIT(type, size) \
8696
{ \
@@ -127,7 +137,11 @@ static inline void _Py_RefcntAdd(PyObject* op, Py_ssize_t n)
127137
_Py_AddRefTotal(_PyThreadState_GET(), n);
128138
#endif
129139
#if !defined(Py_GIL_DISABLED)
140+
#if SIZEOF_VOID_P > 4
141+
op->ob_refcnt += (PY_UINT32_T)n;
142+
#else
130143
op->ob_refcnt += n;
144+
#endif
131145
#else
132146
if (_Py_IsOwnedByCurrentThread(op)) {
133147
uint32_t local = op->ob_ref_local;

Include/object.h

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ whose size is determined when the object is allocated.
7171
#define PyObject_HEAD_INIT(type) \
7272
{ \
7373
0, \
74-
0, \
74+
_Py_STATICALLY_ALLOCATED_FLAG, \
7575
{ 0 }, \
7676
0, \
7777
_Py_IMMORTAL_REFCNT_LOCAL, \
@@ -81,7 +81,7 @@ whose size is determined when the object is allocated.
8181
#else
8282
#define PyObject_HEAD_INIT(type) \
8383
{ \
84-
{ _Py_IMMORTAL_INITIAL_REFCNT }, \
84+
{ _Py_STATIC_IMMORTAL_INITIAL_REFCNT }, \
8585
(type) \
8686
},
8787
#endif
@@ -120,9 +120,19 @@ struct _object {
120120
__pragma(warning(disable: 4201))
121121
#endif
122122
union {
123-
Py_ssize_t ob_refcnt;
124123
#if SIZEOF_VOID_P > 4
125-
PY_UINT32_T ob_refcnt_split[2];
124+
PY_INT64_T ob_refcnt_full; /* This field is needed for efficient initialization with Clang on ARM */
125+
struct {
126+
# if PY_BIG_ENDIAN
127+
PY_UINT32_T ob_flags;
128+
PY_UINT32_T ob_refcnt;
129+
# else
130+
PY_UINT32_T ob_refcnt;
131+
PY_UINT32_T ob_flags;
132+
# endif
133+
};
134+
#else
135+
Py_ssize_t ob_refcnt;
126136
#endif
127137
};
128138
#ifdef _MSC_VER
@@ -142,7 +152,7 @@ struct _object {
142152
// trashcan mechanism as a linked list pointer and by the GC to store the
143153
// computed "gc_refs" refcount.
144154
uintptr_t ob_tid;
145-
uint16_t _padding;
155+
uint16_t ob_flags;
146156
PyMutex ob_mutex; // per-object lock
147157
uint8_t ob_gc_bits; // gc-related state
148158
uint32_t ob_ref_local; // local reference count

Include/refcount.h

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ immortal. The latter should be the only instances that require
1919
cleanup during runtime finalization.
2020
*/
2121

22+
/* Leave the low bits for refcount overflow for old stable ABI code */
23+
#define _Py_STATICALLY_ALLOCATED_FLAG (1 << 7)
24+
2225
#if SIZEOF_VOID_P > 4
2326
/*
2427
In 64+ bit systems, any object whose 32 bit reference count is >= 2**31
@@ -39,7 +42,8 @@ beyond the refcount limit. Immortality checks for reference count decreases will
3942
be done by checking the bit sign flag in the lower 32 bits.
4043
4144
*/
42-
#define _Py_IMMORTAL_INITIAL_REFCNT ((Py_ssize_t)(3UL << 30))
45+
#define _Py_IMMORTAL_INITIAL_REFCNT (3UL << 30)
46+
#define _Py_STATIC_IMMORTAL_INITIAL_REFCNT ((Py_ssize_t)(_Py_IMMORTAL_INITIAL_REFCNT | (((Py_ssize_t)_Py_STATICALLY_ALLOCATED_FLAG) << 32)))
4347

4448
#else
4549
/*
@@ -54,8 +58,10 @@ immortality, but the execution would still be correct.
5458
Reference count increases and decreases will first go through an immortality
5559
check by comparing the reference count field to the minimum immortality refcount.
5660
*/
57-
#define _Py_IMMORTAL_INITIAL_REFCNT ((Py_ssize_t)(3L << 29))
61+
#define _Py_IMMORTAL_INITIAL_REFCNT ((Py_ssize_t)(5L << 28))
5862
#define _Py_IMMORTAL_MINIMUM_REFCNT ((Py_ssize_t)(1L << 30))
63+
#define _Py_STATIC_IMMORTAL_INITIAL_REFCNT ((Py_ssize_t)(7L << 28))
64+
#define _Py_STATIC_IMMORTAL_MINIMUM_REFCNT ((Py_ssize_t)(6L << 28))
5965
#endif
6066

6167
// Py_GIL_DISABLED builds indicate immortal objects using `ob_ref_local`, which is
@@ -123,10 +129,21 @@ static inline Py_ALWAYS_INLINE int _Py_IsImmortal(PyObject *op)
123129
#define _Py_IsImmortal(op) _Py_IsImmortal(_PyObject_CAST(op))
124130

125131

132+
static inline Py_ALWAYS_INLINE int _Py_IsStaticImmortal(PyObject *op)
133+
{
134+
#if defined(Py_GIL_DISABLED) || SIZEOF_VOID_P > 4
135+
return (op->ob_flags & _Py_STATICALLY_ALLOCATED_FLAG) != 0;
136+
#else
137+
return op->ob_refcnt >= _Py_STATIC_IMMORTAL_MINIMUM_REFCNT;
138+
#endif
139+
}
140+
#define _Py_IsStaticImmortal(op) _Py_IsStaticImmortal(_PyObject_CAST(op))
141+
126142
// Py_SET_REFCNT() implementation for stable ABI
127143
PyAPI_FUNC(void) _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt);
128144

129145
static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {
146+
assert(refcnt >= 0);
130147
#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030d0000
131148
// Stable ABI implements Py_SET_REFCNT() as a function call
132149
// on limited C API version 3.13 and newer.
@@ -139,9 +156,12 @@ static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {
139156
if (_Py_IsImmortal(ob)) {
140157
return;
141158
}
142-
143159
#ifndef Py_GIL_DISABLED
160+
#if SIZEOF_VOID_P > 4
161+
ob->ob_refcnt = (PY_UINT32_T)refcnt;
162+
#else
144163
ob->ob_refcnt = refcnt;
164+
#endif
145165
#else
146166
if (_Py_IsOwnedByCurrentThread(ob)) {
147167
if ((size_t)refcnt > (size_t)UINT32_MAX) {
@@ -252,13 +272,13 @@ static inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op)
252272
_Py_atomic_add_ssize(&op->ob_ref_shared, (1 << _Py_REF_SHARED_SHIFT));
253273
}
254274
#elif SIZEOF_VOID_P > 4
255-
PY_UINT32_T cur_refcnt = op->ob_refcnt_split[PY_BIG_ENDIAN];
275+
PY_UINT32_T cur_refcnt = op->ob_refcnt;
256276
if (((int32_t)cur_refcnt) < 0) {
257277
// the object is immortal
258278
_Py_INCREF_IMMORTAL_STAT_INC();
259279
return;
260280
}
261-
op->ob_refcnt_split[PY_BIG_ENDIAN] = cur_refcnt + 1;
281+
op->ob_refcnt = cur_refcnt + 1;
262282
#else
263283
if (_Py_IsImmortal(op)) {
264284
_Py_INCREF_IMMORTAL_STAT_INC();
@@ -354,7 +374,13 @@ static inline void Py_DECREF(PyObject *op)
354374
#elif defined(Py_REF_DEBUG)
355375
static inline void Py_DECREF(const char *filename, int lineno, PyObject *op)
356376
{
377+
#if SIZEOF_VOID_P > 4
378+
/* If an object has been freed, it will have a negative full refcnt
379+
* If it has not it been freed, will have a very large refcnt */
380+
if (op->ob_refcnt_full <= 0 || op->ob_refcnt > (UINT32_MAX - (1<<20))) {
381+
#else
357382
if (op->ob_refcnt <= 0) {
383+
#endif
358384
_Py_NegativeRefcount(filename, lineno, op);
359385
}
360386
if (_Py_IsImmortal(op)) {

Lib/test/test_builtin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2691,7 +2691,7 @@ def __del__(self):
26912691
class ImmortalTests(unittest.TestCase):
26922692

26932693
if sys.maxsize < (1 << 32):
2694-
IMMORTAL_REFCOUNT = 3 << 29
2694+
IMMORTAL_REFCOUNT = 7 << 28
26952695
else:
26962696
IMMORTAL_REFCOUNT = 3 << 30
26972697

Lib/test/test_capi/test_immortal.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from test.support import import_helper
33

44
_testcapi = import_helper.import_module('_testcapi')
5+
_testinternalcapi = import_helper.import_module('_testinternalcapi')
56

67

78
class TestCAPI(unittest.TestCase):
@@ -11,6 +12,21 @@ def test_immortal_builtins(self):
1112
def test_immortal_small_ints(self):
1213
_testcapi.test_immortal_small_ints()
1314

15+
class TestInternalCAPI(unittest.TestCase):
16+
17+
def test_immortal_builtins(self):
18+
for obj in range(-5, 256):
19+
self.assertTrue(_testinternalcapi.is_static_immortal(obj))
20+
self.assertTrue(_testinternalcapi.is_static_immortal(None))
21+
self.assertTrue(_testinternalcapi.is_static_immortal(False))
22+
self.assertTrue(_testinternalcapi.is_static_immortal(True))
23+
self.assertTrue(_testinternalcapi.is_static_immortal(...))
24+
self.assertTrue(_testinternalcapi.is_static_immortal(()))
25+
for obj in range(300, 400):
26+
self.assertFalse(_testinternalcapi.is_static_immortal(obj))
27+
for obj in ([], {}, set()):
28+
self.assertFalse(_testinternalcapi.is_static_immortal(obj))
29+
1430

1531
if __name__ == "__main__":
1632
unittest.main()

Modules/_testinternalcapi.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,15 @@ get_tracked_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored))
20492049
return PyLong_FromInt64(PyInterpreterState_Get()->gc.heap_size);
20502050
}
20512051

2052+
static PyObject *
2053+
is_static_immortal(PyObject *self, PyObject *op)
2054+
{
2055+
if (_Py_IsStaticImmortal(op)) {
2056+
Py_RETURN_TRUE;
2057+
}
2058+
Py_RETURN_FALSE;
2059+
}
2060+
20522061
static PyMethodDef module_functions[] = {
20532062
{"get_configs", get_configs, METH_NOARGS},
20542063
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2146,6 +2155,7 @@ static PyMethodDef module_functions[] = {
21462155
{"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS},
21472156
{"has_deferred_refcount", has_deferred_refcount, METH_O},
21482157
{"get_tracked_heap_size", get_tracked_heap_size, METH_NOARGS},
2158+
{"is_static_immortal", is_static_immortal, METH_O},
21492159
{NULL, NULL} /* sentinel */
21502160
};
21512161

Objects/object.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2475,10 +2475,16 @@ new_reference(PyObject *op)
24752475
{
24762476
// Skip the immortal object check in Py_SET_REFCNT; always set refcnt to 1
24772477
#if !defined(Py_GIL_DISABLED)
2478+
#if SIZEOF_VOID_P > 4
2479+
op->ob_refcnt_full = 1;
2480+
assert(op->ob_refcnt == 1);
2481+
assert(op->ob_flags == 0);
2482+
#else
24782483
op->ob_refcnt = 1;
2484+
#endif
24792485
#else
24802486
op->ob_tid = _Py_ThreadId();
2481-
op->_padding = 0;
2487+
op->ob_flags = 0;
24822488
op->ob_mutex = (PyMutex){ 0 };
24832489
op->ob_gc_bits = 0;
24842490
op->ob_ref_local = 1;
@@ -2515,6 +2521,10 @@ _Py_SetImmortalUntracked(PyObject *op)
25152521
|| PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL_STATIC);
25162522
}
25172523
#endif
2524+
// Check if already immortal to avoid degrading from static immortal to plain immortal
2525+
if (_Py_IsImmortal(op)) {
2526+
return;
2527+
}
25182528
#ifdef Py_GIL_DISABLED
25192529
op->ob_tid = _Py_UNOWNED_TID;
25202530
op->ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL;

0 commit comments

Comments
 (0)