Skip to content
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
2 changes: 2 additions & 0 deletions Include/internal/pycore_gc.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ static inline PyObject* _Py_FROM_GC(PyGC_Head *gc) {
# define _PyGC_BITS_SHARED (1<<4)
# define _PyGC_BITS_ALIVE (1<<5) // Reachable from a known root.
# define _PyGC_BITS_DEFERRED (1<<6) // Use deferred reference counting
# define _PyGC_BITS_IS_GC (1<<7) // Cached result of _PyType_IS_GC()
// for types with a NULL tp_is_gc
#endif

#ifdef Py_GIL_DISABLED
Expand Down
12 changes: 12 additions & 0 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,18 @@ _PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSET(PyObject *op)
static inline int
_PyObject_IS_GC(PyObject *obj)
{
#ifdef Py_GIL_DISABLED
// _PyGC_BITS_IS_GC is cached at object creation time (see new_reference()
// in Objects/object.c) for the common case of a type with a NULL
// tp_is_gc: this avoids dereferencing the type object on every call.
// Only _PyUOpExecutor_Type currently has a non-NULL tp_is_gc, whose
// result can change after creation (e.g. once made immortal), so such
// objects never get the bit set and always fall through to the general
// check below.
if (_PyObject_HAS_GC_BITS(obj, _PyGC_BITS_IS_GC)) {
return 1;
}
#endif
PyTypeObject *type = Py_TYPE(obj);
return (_PyType_IS_GC(type)
&& (type->tp_is_gc == NULL || type->tp_is_gc(obj)));
Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_capi/test_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,43 @@ def test_is_uniquely_referenced(self):
self.assertFalse(_testcapi.is_uniquely_referenced(42))
# CRASHES is_uniquely_referenced(NULL)

class IsGCTest(unittest.TestCase):
"""Test PyObject_IS_GC()"""

class Slotted:
__slots__ = ()

def check_is_gc(self, expected, factory):
# Check both freshly allocated objects and ones obtained after
# churning through a freelist (gh-114733: the free-threaded build
# caches the result of PyObject_IS_GC() at allocation/reinitialization
# time, so both paths must agree with the general, uncached result).
for _ in range(1050):
obj = factory()
self.assertEqual(_testcapi.pyobject_is_gc(obj), expected)
del obj

def test_is_gc_true(self):
self.check_is_gc(True, list)
self.check_is_gc(True, dict)
self.check_is_gc(True, set)
self.check_is_gc(True, tuple)
self.check_is_gc(True, lambda: (1, 2, 3))
self.check_is_gc(True, self.Slotted)

def test_is_gc_false(self):
# object() itself has no GC support: instances hold no references
# and so cannot participate in reference cycles.
self.check_is_gc(False, object)
self.check_is_gc(False, int)
self.check_is_gc(False, float)
self.check_is_gc(False, complex)
self.check_is_gc(False, str)
self.check_is_gc(False, bytes)
self.check_is_gc(False, lambda: 42)
self.check_is_gc(False, lambda: 4.2)


class CAPITest(unittest.TestCase):
def check_negative_refcount(self, code):
# bpo-35059: Check that Py_DECREF() reports the correct filename
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
In the free-threaded build, cache whether an object's type supports cyclic
garbage collection in the object's header at creation time, avoiding a type
dereference on every :c:func:`PyObject_IS_GC` check.
7 changes: 7 additions & 0 deletions Modules/_testcapi/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,12 @@ pyobject_dump(PyObject *self, PyObject *args)
Py_RETURN_NONE;
}

static PyObject *
pyobject_is_gc(PyObject *self, PyObject *obj)
{
return PyBool_FromLong(PyObject_IS_GC(obj));
}

static PyObject *
pysentinel_new(PyObject *self, PyObject *args)
{
Expand Down Expand Up @@ -597,6 +603,7 @@ static PyMethodDef test_methods[] = {
{"pyobject_is_unique_temporary_new_object", pyobject_is_unique_temporary_new_object, METH_NOARGS},
{"test_py_try_inc_ref", test_py_try_inc_ref, METH_NOARGS},
{"test_py_set_immortal", test_py_set_immortal, METH_NOARGS},
{"pyobject_is_gc", pyobject_is_gc, METH_O},
{"test_xincref_doesnt_leak",test_xincref_doesnt_leak, METH_NOARGS},
{"test_incref_doesnt_leak", test_incref_doesnt_leak, METH_NOARGS},
{"test_xdecref_doesnt_leak",test_xdecref_doesnt_leak, METH_NOARGS},
Expand Down
16 changes: 14 additions & 2 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -2741,14 +2741,26 @@ new_reference(PyObject *op)
#else
op->ob_flags = 0;
op->ob_mutex = (PyMutex){ 0 };
// Cache whether the type is a GC type with a static (NULL) tp_is_gc,
// so _PyObject_IS_GC() can skip dereferencing the type object on
// future calls (gh-114733). Py_TYPE(op) is already valid here: callers
// either just set it (_PyObject_Init()) or are reinitializing a
// reused block (realloc, freelist) whose type cannot have changed,
// since types never change their GC-ness after creation. Types with
// a non-NULL tp_is_gc (only _PyUOpExecutor_Type at present) are left
// uncached, since their "is GC" result can change at runtime; they
// always take the slow path in _PyObject_IS_GC().
PyTypeObject *tp = Py_TYPE(op);
uint8_t gc_bits = (_PyType_IS_GC(tp) && tp->tp_is_gc == NULL)
? _PyGC_BITS_IS_GC : 0;
#ifdef _Py_THREAD_SANITIZER
_Py_atomic_store_uintptr_relaxed(&op->ob_tid, _Py_ThreadId());
_Py_atomic_store_uint8_relaxed(&op->ob_gc_bits, 0);
_Py_atomic_store_uint8_relaxed(&op->ob_gc_bits, gc_bits);
_Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 1);
_Py_atomic_store_ssize_relaxed(&op->ob_ref_shared, 0);
#else
op->ob_tid = _Py_ThreadId();
op->ob_gc_bits = 0;
op->ob_gc_bits = gc_bits;
op->ob_ref_local = 1;
op->ob_ref_shared = 0;
#endif
Expand Down
Loading