Skip to content

bpo-40255: Implement Immortal Instances - Optimization 3 #31490

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Include/boolobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ PyAPI_FUNC(int) Py_IsFalse(PyObject *x);
#define Py_IsFalse(x) Py_Is((x), Py_False)

/* Macros for returning Py_True or Py_False, respectively */
#define Py_RETURN_TRUE return Py_NewRef(Py_True)
#define Py_RETURN_FALSE return Py_NewRef(Py_False)
#define Py_RETURN_TRUE return Py_True
#define Py_RETURN_FALSE return Py_False

/* Function to return a bool from a C long */
PyAPI_FUNC(PyObject *) PyBool_FromLong(long);
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extern "C" {

#define _PyObject_IMMORTAL_INIT(type) \
{ \
.ob_refcnt = 999999999, \
.ob_refcnt = _Py_IMMORTAL_REFCNT, \
.ob_type = type, \
}
#define _PyVarObject_IMMORTAL_INIT(type, size) \
Expand Down
12 changes: 7 additions & 5 deletions Include/moduleobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ typedef struct PyModuleDef_Base {
PyObject* m_copy;
} PyModuleDef_Base;

#define PyModuleDef_HEAD_INIT { \
PyObject_HEAD_INIT(NULL) \
NULL, /* m_init */ \
0, /* m_index */ \
NULL, /* m_copy */ \
// TODO(eduardo-elizondo): This is only used to simplify the review of GH-19474
// Rather than changing this API, we'll introduce PyModuleDef_HEAD_IMMORTAL_INIT
#define PyModuleDef_HEAD_INIT { \
PyObject_HEAD_IMMORTAL_INIT(NULL) \
NULL, /* m_init */ \
0, /* m_index */ \
NULL, /* m_copy */ \
}

struct PyModuleDef_Slot;
Expand Down
48 changes: 46 additions & 2 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,34 @@ typedef struct _typeobject PyTypeObject;
/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD PyObject ob_base;

/*
Immortalization:

This marks the reference count bit that will be used to define immortality.
The GC bit-shifts refcounts left by two, and after that shift it still needs
to be larger than zero, so it's placed after the first three high bits.

For backwards compatibility the actual reference count of an immortal instance
is set to higher than just the immortal bit. This will ensure that the immortal
bit will remain active, even with extensions compiled without the updated checks
in Py_INCREF and Py_DECREF. This can be safely changed to a smaller value if
additional bits are needed in the reference count field.
*/
#define _Py_IMMORTAL_BIT_OFFSET (8 * sizeof(Py_ssize_t) - 4)
#define _Py_IMMORTAL_BIT (1LL << _Py_IMMORTAL_BIT_OFFSET)
#define _Py_IMMORTAL_REFCNT (_Py_IMMORTAL_BIT + (_Py_IMMORTAL_BIT / 2))

#define PyObject_HEAD_INIT(type) \
{ _PyObject_EXTRA_INIT \
1, type },

#define PyObject_HEAD_IMMORTAL_INIT(type) \
{ _PyObject_EXTRA_INIT _Py_IMMORTAL_REFCNT, type },

// TODO(eduardo-elizondo): This is only used to simplify the review of GH-19474
// Rather than changing this API, we'll introduce PyVarObject_HEAD_IMMORTAL_INIT
#define PyVarObject_HEAD_INIT(type, size) \
{ PyObject_HEAD_INIT(type) size },
{ PyObject_HEAD_IMMORTAL_INIT(type) size },

/* PyObject_VAR_HEAD defines the initial segment of all variable-size
* container objects. These end with a declaration of an array with 1
Expand Down Expand Up @@ -145,6 +167,19 @@ static inline Py_ssize_t Py_SIZE(const PyVarObject *ob) {
}
#define Py_SIZE(ob) Py_SIZE(_PyVarObject_CAST_CONST(ob))

PyAPI_FUNC(PyObject *) _PyGC_TransitiveImmortalize(PyObject *obj);

static inline int _Py_IsImmortal(PyObject *op)
{
return (op->ob_refcnt & _Py_IMMORTAL_BIT) != 0;
}

static inline void _Py_SetImmortal(PyObject *op)
{
if (op) {
op->ob_refcnt = _Py_IMMORTAL_REFCNT;
}
}

static inline int Py_IS_TYPE(const PyObject *ob, const PyTypeObject *type) {
// bpo-44378: Don't use Py_TYPE() since Py_TYPE() requires a non-const
Expand All @@ -155,6 +190,9 @@ static inline int Py_IS_TYPE(const PyObject *ob, const PyTypeObject *type) {


static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {
if (_Py_IsImmortal(ob)) {
return;
}
ob->ob_refcnt = refcnt;
}
#define Py_SET_REFCNT(ob, refcnt) Py_SET_REFCNT(_PyObject_CAST(ob), refcnt)
Expand Down Expand Up @@ -483,6 +521,9 @@ static inline void Py_INCREF(PyObject *op)
#else
// Non-limited C API and limited C API for Python 3.9 and older access
// directly PyObject.ob_refcnt.
if (_Py_IsImmortal(op)) {
return;
}
#ifdef Py_REF_DEBUG
_Py_RefTotal++;
#endif
Expand All @@ -503,6 +544,9 @@ static inline void Py_DECREF(
#else
// Non-limited C API and limited C API for Python 3.9 and older access
// directly PyObject.ob_refcnt.
if (_Py_IsImmortal(op)) {
return;
}
#ifdef Py_REF_DEBUG
_Py_RefTotal--;
#endif
Expand Down Expand Up @@ -627,7 +671,7 @@ PyAPI_FUNC(int) Py_IsNone(PyObject *x);
#define Py_IsNone(x) Py_Is((x), Py_None)

/* Macro for returning Py_None from a function */
#define Py_RETURN_NONE return Py_NewRef(Py_None)
#define Py_RETURN_NONE return Py_None

/*
Py_NotImplemented is a singleton used to signal that an operation is
Expand Down
3 changes: 2 additions & 1 deletion Lib/ctypes/test/test_python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ def test_PyLong_Long(self):
pythonapi.PyLong_AsLong.restype = c_long

res = pythonapi.PyLong_AsLong(42)
self.assertEqual(grc(res), ref42 + 1)
# Small int refcnts don't change
self.assertEqual(grc(res), ref42)
del res
self.assertEqual(grc(42), ref42)

Expand Down
25 changes: 24 additions & 1 deletion Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from types import AsyncGeneratorType, FunctionType
from operator import neg
from test import support
from test.support import (swap_attr, maybe_get_event_loop_policy)
from test.support import (cpython_only, swap_attr, maybe_get_event_loop_policy)
from test.support.os_helper import (EnvironmentVarGuard, TESTFN, unlink)
from test.support.script_helper import assert_python_ok
from test.support.warnings_helper import check_warnings
Expand Down Expand Up @@ -2214,6 +2214,29 @@ def __del__(self):
self.assertEqual(["before", "after"], out.decode().splitlines())


@cpython_only
class ImmortalTests(unittest.TestCase):
def test_immortal(self):
none_refcount = sys.getrefcount(None)
true_refcount = sys.getrefcount(True)
false_refcount = sys.getrefcount(False)
smallint_refcount = sys.getrefcount(100)

# Assert that all of these immortal instances have large ref counts
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Assert that all of these immortal instances have large ref counts
# Assert that all of these immortal instances have large ref counts.

self.assertGreater(none_refcount, 1e8)
self.assertGreater(true_refcount, 1e8)
self.assertGreater(false_refcount, 1e8)
self.assertGreater(smallint_refcount, 1e8)

# Confirm that the refcount doesn't change even with a new ref to them
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Confirm that the refcount doesn't change even with a new ref to them
# Confirm that the refcount doesn't change even with a new ref to them.

l = [None, True, False, 100]
self.assertEqual(sys.getrefcount(None), none_refcount)
self.assertEqual(sys.getrefcount(True), true_refcount)
self.assertEqual(sys.getrefcount(False), false_refcount)
self.assertEqual(sys.getrefcount(100), smallint_refcount)



class TestType(unittest.TestCase):
def test_new_type(self):
A = type('A', (), {})
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ def __del__(self):
rc, out, err = assert_python_ok('-c', code)
self.assertEqual(out.strip(), b'__del__ called')

@unittest.skipIf(True, 'Fixed in Optimization 2 with topological destruction')
def test_gc_ordinary_module_at_shutdown(self):
# Same as above, but with a non-__main__ module.
with temp_dir() as script_dir:
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ def test_module_repr_source(self):
self.assertEqual(r[-len(ends_with):], ends_with,
'{!r} does not end with {!r}'.format(r, ends_with))

@unittest.skipIf(True, 'Fixed in Optimization 2 with topological destruction')
def test_module_finalization_at_shutdown(self):
# Module globals and builtins should still be available during shutdown
rc, out, err = assert_python_ok("-c", "from test import final_a")
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_regrtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,7 @@ class RefLeakTest(unittest.TestCase):
def test_leak(self):
GLOBAL_LIST.append(object())
""")
self.check_leak(code, 'references')
self.check_leak(code, 'memory blocks')

@unittest.skipUnless(Py_DEBUG, 'need a debug build')
def test_huntrleaks_fd_leak(self):
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ def test_refcount(self):
self.assertRaises(TypeError, sys.getrefcount)
c = sys.getrefcount(None)
n = None
self.assertEqual(sys.getrefcount(None), c+1)
# Singleton refcnts don't change
self.assertEqual(sys.getrefcount(None), c)
del n
self.assertEqual(sys.getrefcount(None), c)
if hasattr(sys, "gettotalrefcount"):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
This introduces Immortal Instances which allows objects to bypass reference
counting and remain alive throughout the execution of the runtime
50 changes: 48 additions & 2 deletions Modules/gcmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,12 @@ gc_list_move(PyGC_Head *node, PyGC_Head *list)
/* Unlink from current list. */
PyGC_Head *from_prev = GC_PREV(node);
PyGC_Head *from_next = GC_NEXT(node);
_PyGCHead_SET_NEXT(from_prev, from_next);
_PyGCHead_SET_PREV(from_next, from_prev);
if (from_next) {
_PyGCHead_SET_NEXT(from_prev, from_next);
}
if (from_prev) {
_PyGCHead_SET_PREV(from_next, from_prev);
}

/* Relink at end of new list. */
// list must not have flags. So we can skip macros.
Expand Down Expand Up @@ -1953,6 +1957,48 @@ gc_get_freeze_count_impl(PyObject *module)
}


static int
immortalize_object(PyObject *obj, PyGC_Head *permanent_gen)
{
if (_Py_IsImmortal(obj)) {
return 0;
}

_Py_SetImmortal(obj);
/* Special case for PyCodeObjects since they don't have a tp_traverse */
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/* Special case for PyCodeObjects since they don't have a tp_traverse */
// Special case for PyCodeObjects since they don't have a tp_traverse.

if (PyCode_Check(obj)) {
PyCodeObject *code = (PyCodeObject *)obj;
_Py_SetImmortal(code->co_code);
_Py_SetImmortal(code->co_consts);
_Py_SetImmortal(code->co_names);
_Py_SetImmortal(code->co_varnames);
_Py_SetImmortal(code->co_freevars);
_Py_SetImmortal(code->co_cellvars);
Copy link
Member

Choose a reason for hiding this comment

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

Make contents of these tuples immortal too.
Especially, co_consts is important, because LOAD_CONST will INCREF.

_Py_SetImmortal(code->co_filename);
_Py_SetImmortal(code->co_name);
_Py_SetImmortal(code->co_linetable);
}

PyTypeObject* tp = Py_TYPE(obj);
if (tp->tp_traverse) {
gc_list_move(AS_GC(obj), permanent_gen);
tp->tp_traverse(obj, (visitproc)immortalize_object, permanent_gen);
}
return 0;
}

PyObject *
_PyGC_TransitiveImmortalize(PyObject *obj) {
_Py_SetImmortal(obj);
Py_TYPE(obj)->tp_traverse(
obj,
(visitproc)immortalize_object,
&_PyThreadState_GET()->interp->gc.permanent_generation.head
);
Py_RETURN_NONE;
}


PyDoc_STRVAR(gc__doc__,
"This module provides access to the garbage collector for reference cycles.\n"
"\n"
Expand Down
4 changes: 1 addition & 3 deletions Objects/longobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ static PyObject *
get_small_int(sdigit ival)
{
assert(IS_SMALL_INT(ival));
PyObject *v = (PyObject *)&_PyLong_SMALL_INTS[_PY_NSMALLNEGINTS + ival];
Py_INCREF(v);
return v;
return (PyObject *)&_PyLong_SMALL_INTS[_PY_NSMALLNEGINTS + ival];
}

static PyLongObject *
Expand Down
7 changes: 5 additions & 2 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -1705,7 +1705,8 @@ PyTypeObject _PyNone_Type = {

PyObject _Py_NoneStruct = {
_PyObject_EXTRA_INIT
1, &_PyNone_Type
_Py_IMMORTAL_REFCNT,
&_PyNone_Type
};

/* NotImplemented is an object that can be used to signal that an
Expand Down Expand Up @@ -1994,7 +1995,9 @@ _Py_NewReference(PyObject *op)
#ifdef Py_REF_DEBUG
_Py_RefTotal++;
#endif
Py_SET_REFCNT(op, 1);
/* Do not use Py_SET_REFCNT to skip the Immortal Instance check. This
* API guarantees that an instance will always be set to a refcnt of 1 */
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* API guarantees that an instance will always be set to a refcnt of 1 */
* API guarantees that an instance will always be set to a refcnt of 1. */

op->ob_refcnt = 1;
#ifdef Py_TRACE_REFS
_Py_AddToAllObjects(op, 1);
#endif
Expand Down
4 changes: 4 additions & 0 deletions Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,10 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
if (mod == NULL) {
goto error;
}
// Immortalize top level modules
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Immortalize top level modules
// Immortalize top level modules.

if (tstate->recursion_limit - tstate->recursion_remaining == 1) {
_PyGC_TransitiveImmortalize(mod);
}
}

has_from = 0;
Expand Down