Skip to content
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

bpo-40255: Implement Immortal Instances - Optimizations Combined #31491

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: 2 additions & 0 deletions Include/cpython/sysmodule.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ PyAPI_FUNC(PyObject *) _PySys_GetAttr(PyThreadState *tstate,
PyAPI_FUNC(PyObject *) _PySys_GetObjectId(_Py_Identifier *key);
PyAPI_FUNC(int) _PySys_SetObjectId(_Py_Identifier *key, PyObject *);

PyAPI_FUNC(PyObject *) _PySys_StdlibModuleNameList(void);

PyAPI_FUNC(size_t) _PySys_GetSizeOf(PyObject *);

typedef int(*Py_AuditHookFunction)(const char *, PyObject *, void *);
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
13 changes: 8 additions & 5 deletions Include/moduleobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Py_DEPRECATED(3.2) PyAPI_FUNC(const char *) PyModule_GetFilename(PyObject *);
PyAPI_FUNC(PyObject *) PyModule_GetFilenameObject(PyObject *);
#ifndef Py_LIMITED_API
PyAPI_FUNC(void) _PyModule_Clear(PyObject *);
PyAPI_FUNC(void) _PyModule_PhasedClear(PyObject *, int phase);
PyAPI_FUNC(void) _PyModule_ClearDict(PyObject *);
PyAPI_FUNC(int) _PyModuleSpec_IsInitializing(PyObject *);
#endif
Expand All @@ -48,11 +49,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
49 changes: 47 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,20 @@ static inline Py_ssize_t Py_SIZE(const PyVarObject *ob) {
}
#define Py_SIZE(ob) Py_SIZE(_PyVarObject_CAST_CONST(ob))

PyAPI_FUNC(PyObject *) _PyGC_ImmortalizeHeap(void);
PyAPI_FUNC(PyObject *) _PyGC_TransitiveImmortalize(PyObject *obj);
Copy link
Member

Choose a reason for hiding this comment

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

(This name is awkward, I'd expect some variation on "immortalize transitively".)


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 +191,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 +522,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 +545,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 +672,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
5 changes: 4 additions & 1 deletion Lib/concurrent/futures/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ def __init__(self, executor):
self.thread_wakeup = executor._executor_manager_thread_wakeup
self.shutdown_lock = executor._shutdown_lock

# Make mp.util.debug available even during runtime shutdown
self._debugp = mp.util.debug
# A weakref.ref to the ProcessPoolExecutor that owns this thread. Used
# to determine if the ProcessPoolExecutor has been garbage collected
# and that the manager can exit.
Expand All @@ -297,8 +299,9 @@ def __init__(self, executor):
def weakref_cb(_,
thread_wakeup=self.thread_wakeup,
shutdown_lock=self.shutdown_lock):
mp.util.debug('Executor collected: triggering callback for'
self._debugp('Executor collected: triggering callback for'
' QueueManager wakeup')
# ./python Lib/test/test_concurrent_futures.py ProcessPoolForkProcessPoolShutdownTest.test_interpreter_shutdown
with shutdown_lock:
thread_wakeup.wakeup()

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
4 changes: 3 additions & 1 deletion Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,8 @@ def __init__(self, name, level=NOTSET):
self.handlers = []
self.disabled = False
self._cache = {}
# Make normcase available even during runtime shutdown
self._normcase = os.path.normcase

def setLevel(self, level):
"""
Expand Down Expand Up @@ -1569,7 +1571,7 @@ def findCaller(self, stack_info=False, stacklevel=1):
rv = "(unknown file)", 0, "(unknown function)", None
while hasattr(f, "f_code"):
co = f.f_code
filename = os.path.normcase(co.co_filename)
filename = self._normcase(co.co_filename)
if filename == _srcfile:
f = f.f_back
continue
Expand Down
8 changes: 6 additions & 2 deletions Lib/multiprocessing/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1334,10 +1334,14 @@ def __init__(self, *args, **kwargs):
from . import resource_tracker
resource_tracker.ensure_running()
BaseManager.__init__(self, *args, **kwargs)
util.debug(f"{self.__class__.__name__} created by pid {getpid()}")

# Make util.debug available even during runtime shutdown
self._debugp = util.debug

self._debugp(f"{self.__class__.__name__} created by pid {getpid()}")

def __del__(self):
util.debug(f"{self.__class__.__name__}.__del__ by pid {getpid()}")
self._debugp(f"{self.__class__.__name__}.__del__ by pid {getpid()}")

def get_server(self):
'Better than monkeypatching for now; merge into Server ultimately'
Expand Down
5 changes: 4 additions & 1 deletion Lib/tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,9 @@ def __init__(self, suffix=None, prefix=None, dir=None,
warn_message="Implicitly cleaning up {!r}".format(self),
ignore_errors=self._ignore_cleanup_errors)

# Make _os.path.exists available even during runtime shutdown
self._exists = _os.path.exists

@classmethod
def _rmtree(cls, name, ignore_errors=False):
def onerror(func, path, exc_info):
Expand Down Expand Up @@ -850,7 +853,7 @@ def __exit__(self, exc, value, tb):
self.cleanup()

def cleanup(self):
if self._finalizer.detach() or _os.path.exists(self.name):
if self._finalizer.detach() or self._exists(self.name):
self._rmtree(self.name, ignore_errors=self._ignore_cleanup_errors)

__class_getitem__ = classmethod(_types.GenericAlias)
11 changes: 6 additions & 5 deletions Lib/test/final_a.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
"""

import shutil
import sys
import test.final_b

x = 'a'

class C:
def __del__(self):
def __del__(self, sys=sys):
# Inspect module globals and builtins
print("x =", x)
print("final_b.x =", test.final_b.x)
print("shutil.rmtree =", getattr(shutil.rmtree, '__name__', None))
print("len =", getattr(len, '__name__', None))
print("x =", x, file=sys.stderr)
print("final_b.x =", test.final_b.x, file=sys.stderr)
print("shutil.rmtree =", getattr(shutil.rmtree, '__name__', None), sys.stderr)
print("len =", getattr(len, '__name__', None), sys.stderr)

c = C()
_underscored = C()
11 changes: 6 additions & 5 deletions Lib/test/final_b.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
"""

import shutil
import sys
import test.final_a

x = 'b'

class C:
def __del__(self):
def __del__(self, sys=sys):
# Inspect module globals and builtins
print("x =", x)
print("final_a.x =", test.final_a.x)
print("shutil.rmtree =", getattr(shutil.rmtree, '__name__', None))
print("len =", getattr(len, '__name__', None))
print("x =", x, file=sys.stderr)
print("final_a.x =", test.final_a.x, file=sys.stderr)
print("shutil.rmtree =", getattr(shutil.rmtree, '__name__', None), file=sys.stderr)
print("len =", getattr(len, '__name__', None), file=sys.stderr)

c = C()
_underscored = C()
33 changes: 28 additions & 5 deletions 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 @@ -2189,11 +2189,11 @@ def test_cleanup(self):
import sys

class C:
def __del__(self):
print("before")
def __del__(self, sys=sys):
print("before", file=sys.stderr)
# Check that builtins still exist
len(())
print("after")
print("after", file=sys.stderr)

c = C()
# Make this module survive until builtins and sys are cleaned
Expand All @@ -2211,7 +2211,30 @@ def __del__(self):
# implemented in Python
rc, out, err = assert_python_ok("-c", code,
PYTHONIOENCODING="ascii")
self.assertEqual(["before", "after"], out.decode().splitlines())
self.assertEqual(["before", "after"], err.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
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
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):
Expand Down
21 changes: 12 additions & 9 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,22 +714,24 @@ def test_gc_main_module_at_shutdown(self):
# Create a reference cycle through the __main__ module and check
# it gets collected at interpreter shutdown.
code = """if 1:
import sys
class C:
def __del__(self):
print('__del__ called')
def __del__(self, sys=sys):
print('__del__ called', file=sys.stderr)
l = [C()]
l.append(l)
"""
rc, out, err = assert_python_ok('-c', code)
self.assertEqual(out.strip(), b'__del__ called')
self.assertEqual(err.strip(), b'__del__ called')

def test_gc_ordinary_module_at_shutdown(self):
# Same as above, but with a non-__main__ module.
with temp_dir() as script_dir:
module = """if 1:
import sys
class C:
def __del__(self):
print('__del__ called')
def __del__(self, sys=sys):
print('__del__ called', file=sys.stderr)
l = [C()]
l.append(l)
"""
Expand All @@ -740,21 +742,22 @@ def __del__(self):
""" % (script_dir,)
make_script(script_dir, 'gctest', module)
rc, out, err = assert_python_ok('-c', code)
self.assertEqual(out.strip(), b'__del__ called')
self.assertEqual(err.strip(), b'')

def test_global_del_SystemExit(self):
code = """if 1:
import sys
class ClassWithDel:
def __del__(self):
print('__del__ called')
def __del__(self, sys=sys):
print('__del__ called', file=sys.stderr)
a = ClassWithDel()
a.link = a
raise SystemExit(0)"""
self.addCleanup(unlink, TESTFN)
with open(TESTFN, 'w', encoding="utf-8") as script:
script.write(code)
rc, out, err = assert_python_ok(TESTFN)
self.assertEqual(out.strip(), b'__del__ called')
self.assertEqual(err.strip(), b'__del__ called')

def test_get_stats(self):
stats = gc.get_stats()
Expand Down
Loading