Skip to content

GH-125174: Mark objects as statically allocated. #127797

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 11, 2024
16 changes: 15 additions & 1 deletion Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,24 @@
#define _PyObject_HEAD_INIT(type) \
{ \
.ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL, \
.ob_flags = _Py_STATICALLY_ALLOCATED_FLAG, \
.ob_type = (type) \
}
#else
#if SIZEOF_VOID_P > 4
#define _PyObject_HEAD_INIT(type) \
{ \
.ob_refcnt = _Py_IMMORTAL_INITIAL_REFCNT, \
.ob_refcnt = _Py_IMMORTAL_INITIAL_REFCNT, \
.ob_flags = _Py_STATICALLY_ALLOCATED_FLAG, \
.ob_type = (type) \
}
#else
#define _PyObject_HEAD_INIT(type) \
{ \
.ob_refcnt = _Py_STATIC_IMMORTAL_INITIAL_REFCNT, \
.ob_type = (type) \
}
#endif
#endif
#define _PyVarObject_HEAD_INIT(type, size) \
{ \
Expand Down Expand Up @@ -127,7 +137,11 @@
_Py_AddRefTotal(_PyThreadState_GET(), n);
#endif
#if !defined(Py_GIL_DISABLED)
#if SIZEOF_VOID_P > 4
op->ob_refcnt += (PY_UINT32_T)n;
#else
op->ob_refcnt += n;
#endif
#else
if (_Py_IsOwnedByCurrentThread(op)) {
uint32_t local = op->ob_ref_local;
Expand Down Expand Up @@ -179,7 +193,7 @@
op->ob_ref_local = 0;
op->ob_ref_shared = _Py_REF_SHARED(refcnt, _Py_REF_MERGED);
#else
op->ob_refcnt = refcnt;

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (arm64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]

Check warning on line 196 in Include/internal/pycore_object.h

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'=': conversion from 'Py_ssize_t' to 'uint32_t', possible loss of data [D:\a\cpython\cpython\PCbuild\pythoncore.vcxproj]
#endif
}
}
Expand Down
20 changes: 15 additions & 5 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ whose size is determined when the object is allocated.
#define PyObject_HEAD_INIT(type) \
{ \
0, \
0, \
_Py_STATICALLY_ALLOCATED_FLAG, \
{ 0 }, \
0, \
_Py_IMMORTAL_REFCNT_LOCAL, \
Expand All @@ -81,7 +81,7 @@ whose size is determined when the object is allocated.
#else
#define PyObject_HEAD_INIT(type) \
{ \
{ _Py_IMMORTAL_INITIAL_REFCNT }, \
{ _Py_STATIC_IMMORTAL_INITIAL_REFCNT }, \
(type) \
},
#endif
Expand Down Expand Up @@ -120,9 +120,19 @@ struct _object {
__pragma(warning(disable: 4201))
#endif
union {
Py_ssize_t ob_refcnt;
#if SIZEOF_VOID_P > 4
PY_UINT32_T ob_refcnt_split[2];
PY_INT64_T ob_refcnt_full; /* This field is needed for efficient initialization with Clang on ARM */
struct {
# if PY_BIG_ENDIAN
PY_UINT32_T ob_flags;
PY_UINT32_T ob_refcnt;
# else
PY_UINT32_T ob_refcnt;
PY_UINT32_T ob_flags;
# endif
};
#else
Py_ssize_t ob_refcnt;
#endif
};
#ifdef _MSC_VER
Expand All @@ -142,7 +152,7 @@ struct _object {
// trashcan mechanism as a linked list pointer and by the GC to store the
// computed "gc_refs" refcount.
uintptr_t ob_tid;
uint16_t _padding;
uint16_t ob_flags;
PyMutex ob_mutex; // per-object lock
uint8_t ob_gc_bits; // gc-related state
uint32_t ob_ref_local; // local reference count
Expand Down
36 changes: 31 additions & 5 deletions Include/refcount.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ immortal. The latter should be the only instances that require
cleanup during runtime finalization.
*/

/* Leave the low bits for refcount overflow for old stable ABI code */
#define _Py_STATICALLY_ALLOCATED_FLAG (1 << 7)

#if SIZEOF_VOID_P > 4
/*
In 64+ bit systems, any object whose 32 bit reference count is >= 2**31
Expand All @@ -39,7 +42,8 @@ beyond the refcount limit. Immortality checks for reference count decreases will
be done by checking the bit sign flag in the lower 32 bits.

*/
#define _Py_IMMORTAL_INITIAL_REFCNT ((Py_ssize_t)(3UL << 30))
#define _Py_IMMORTAL_INITIAL_REFCNT (3UL << 30)
#define _Py_STATIC_IMMORTAL_INITIAL_REFCNT ((Py_ssize_t)(_Py_IMMORTAL_INITIAL_REFCNT | (((Py_ssize_t)_Py_STATICALLY_ALLOCATED_FLAG) << 32)))

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

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


static inline Py_ALWAYS_INLINE int _Py_IsStaticImmortal(PyObject *op)
{
#if defined(Py_GIL_DISABLED) || SIZEOF_VOID_P > 4
return (op->ob_flags & _Py_STATICALLY_ALLOCATED_FLAG) != 0;
#else
return op->ob_refcnt >= _Py_STATIC_IMMORTAL_MINIMUM_REFCNT;
#endif
}
#define _Py_IsStaticImmortal(op) _Py_IsStaticImmortal(_PyObject_CAST(op))

// Py_SET_REFCNT() implementation for stable ABI
PyAPI_FUNC(void) _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt);

static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {
assert(refcnt >= 0);
#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030d0000
// Stable ABI implements Py_SET_REFCNT() as a function call
// on limited C API version 3.13 and newer.
Expand All @@ -139,9 +156,12 @@ static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {
if (_Py_IsImmortal(ob)) {
return;
}

#ifndef Py_GIL_DISABLED
#if SIZEOF_VOID_P > 4
ob->ob_refcnt = (PY_UINT32_T)refcnt;
#else
ob->ob_refcnt = refcnt;
#endif
#else
if (_Py_IsOwnedByCurrentThread(ob)) {
if ((size_t)refcnt > (size_t)UINT32_MAX) {
Expand Down Expand Up @@ -252,13 +272,13 @@ static inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op)
_Py_atomic_add_ssize(&op->ob_ref_shared, (1 << _Py_REF_SHARED_SHIFT));
}
#elif SIZEOF_VOID_P > 4
PY_UINT32_T cur_refcnt = op->ob_refcnt_split[PY_BIG_ENDIAN];
PY_UINT32_T cur_refcnt = op->ob_refcnt;
if (((int32_t)cur_refcnt) < 0) {
// the object is immortal
_Py_INCREF_IMMORTAL_STAT_INC();
return;
}
op->ob_refcnt_split[PY_BIG_ENDIAN] = cur_refcnt + 1;
op->ob_refcnt = cur_refcnt + 1;
#else
if (_Py_IsImmortal(op)) {
_Py_INCREF_IMMORTAL_STAT_INC();
Expand Down Expand Up @@ -354,7 +374,13 @@ static inline void Py_DECREF(PyObject *op)
#elif defined(Py_REF_DEBUG)
static inline void Py_DECREF(const char *filename, int lineno, PyObject *op)
{
#if SIZEOF_VOID_P > 4
/* If an object has been freed, it will have a negative full refcnt
* If it has not it been freed, will have a very large refcnt */
if (op->ob_refcnt_full <= 0 || op->ob_refcnt > (UINT32_MAX - (1<<20))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently UINT32_MAX is not defined in all builds, so this breaks a buildbot.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use UINT32_MAX all over the place, so it must be defined. Maybe we're missing a #include somewhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's a C++ build that failing, which might explain it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#else
if (op->ob_refcnt <= 0) {
#endif
_Py_NegativeRefcount(filename, lineno, op);
}
if (_Py_IsImmortal(op)) {
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2691,7 +2691,7 @@ def __del__(self):
class ImmortalTests(unittest.TestCase):

if sys.maxsize < (1 << 32):
IMMORTAL_REFCOUNT = 3 << 29
IMMORTAL_REFCOUNT = 7 << 28
else:
IMMORTAL_REFCOUNT = 3 << 30

Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_capi/test_immortal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from test.support import import_helper

_testcapi = import_helper.import_module('_testcapi')
_testinternalcapi = import_helper.import_module('_testinternalcapi')


class TestCAPI(unittest.TestCase):
Expand All @@ -11,6 +12,21 @@ def test_immortal_builtins(self):
def test_immortal_small_ints(self):
_testcapi.test_immortal_small_ints()

class TestInternalCAPI(unittest.TestCase):

def test_immortal_builtins(self):
for obj in range(-5, 256):
self.assertTrue(_testinternalcapi.is_static_immortal(obj))
self.assertTrue(_testinternalcapi.is_static_immortal(None))
self.assertTrue(_testinternalcapi.is_static_immortal(False))
self.assertTrue(_testinternalcapi.is_static_immortal(True))
self.assertTrue(_testinternalcapi.is_static_immortal(...))
self.assertTrue(_testinternalcapi.is_static_immortal(()))
for obj in range(300, 400):
self.assertFalse(_testinternalcapi.is_static_immortal(obj))
for obj in ([], {}, set()):
self.assertFalse(_testinternalcapi.is_static_immortal(obj))


if __name__ == "__main__":
unittest.main()
10 changes: 10 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -2082,6 +2082,15 @@ get_tracked_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored))
return PyLong_FromInt64(PyInterpreterState_Get()->gc.heap_size);
}

static PyObject *
is_static_immortal(PyObject *self, PyObject *op)
{
if (_Py_IsStaticImmortal(op)) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
}

static PyMethodDef module_functions[] = {
{"get_configs", get_configs, METH_NOARGS},
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
Expand Down Expand Up @@ -2180,6 +2189,7 @@ static PyMethodDef module_functions[] = {
{"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS},
{"has_deferred_refcount", has_deferred_refcount, METH_O},
{"get_tracked_heap_size", get_tracked_heap_size, METH_NOARGS},
{"is_static_immortal", is_static_immortal, METH_O},
{NULL, NULL} /* sentinel */
};

Expand Down
12 changes: 11 additions & 1 deletion Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -2475,10 +2475,16 @@ new_reference(PyObject *op)
{
// Skip the immortal object check in Py_SET_REFCNT; always set refcnt to 1
#if !defined(Py_GIL_DISABLED)
#if SIZEOF_VOID_P > 4
op->ob_refcnt_full = 1;
assert(op->ob_refcnt == 1);
assert(op->ob_flags == 0);
#else
op->ob_refcnt = 1;
#endif
#else
op->ob_tid = _Py_ThreadId();
op->_padding = 0;
op->ob_flags = 0;
op->ob_mutex = (PyMutex){ 0 };
op->ob_gc_bits = 0;
op->ob_ref_local = 1;
Expand Down Expand Up @@ -2515,6 +2521,10 @@ _Py_SetImmortalUntracked(PyObject *op)
|| PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL_STATIC);
}
#endif
// Check if already immortal to avoid degrading from static immortal to plain immortal
if (_Py_IsImmortal(op)) {
return;
}
#ifdef Py_GIL_DISABLED
op->ob_tid = _Py_UNOWNED_TID;
op->ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL;
Expand Down
Loading