Skip to content

Commit d00878b

Browse files
gh-123619: Add an unstable C API function for enabling deferred reference counting (GH-123635)
Co-authored-by: Sam Gross <[email protected]>
1 parent 29b5323 commit d00878b

File tree

8 files changed

+128
-1
lines changed

8 files changed

+128
-1
lines changed

Doc/c-api/object.rst

+24
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,27 @@ Object Protocol
575575
has the :c:macro:`Py_TPFLAGS_MANAGED_DICT` flag set.
576576
577577
.. versionadded:: 3.13
578+
579+
.. c:function:: int PyUnstable_Object_EnableDeferredRefcount(PyObject *obj)
580+
581+
Enable `deferred reference counting <https://peps.python.org/pep-0703/#deferred-reference-counting>`_ on *obj*,
582+
if supported by the runtime. In the :term:`free-threaded <free threading>` build,
583+
this allows the interpreter to avoid reference count adjustments to *obj*,
584+
which may improve multi-threaded performance. The tradeoff is
585+
that *obj* will only be deallocated by the tracing garbage collector.
586+
587+
This function returns ``1`` if deferred reference counting is enabled on *obj*
588+
(including when it was enabled before the call),
589+
and ``0`` if deferred reference counting is not supported or if the hint was
590+
ignored by the runtime. This function is thread-safe, and cannot fail.
591+
592+
This function does nothing on builds with the :term:`GIL` enabled, which do
593+
not support deferred reference counting. This also does nothing if *obj* is not
594+
an object tracked by the garbage collector (see :func:`gc.is_tracked` and
595+
:c:func:`PyObject_GC_IsTracked`).
596+
597+
This function is intended to be used soon after *obj* is created,
598+
by the code that creates it.
599+
600+
.. versionadded:: next
601+

Doc/whatsnew/3.14.rst

+3
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,9 @@ New features
890890
* Add :c:func:`PyType_Freeze` function to make a type immutable.
891891
(Contributed by Victor Stinner in :gh:`121654`.)
892892

893+
* Add :c:func:`PyUnstable_Object_EnableDeferredRefcount` for enabling
894+
deferred reference counting, as outlined in :pep:`703`.
895+
893896
Porting to Python 3.14
894897
----------------------
895898

Include/cpython/object.h

+7
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,10 @@ typedef enum {
527527
typedef int (*PyRefTracer)(PyObject *, PyRefTracerEvent event, void *);
528528
PyAPI_FUNC(int) PyRefTracer_SetTracer(PyRefTracer tracer, void *data);
529529
PyAPI_FUNC(PyRefTracer) PyRefTracer_GetTracer(void**);
530+
531+
/* Enable PEP-703 deferred reference counting on the object.
532+
*
533+
* Returns 1 if deferred reference counting was successfully enabled, and
534+
* 0 if the runtime ignored it. This function cannot fail.
535+
*/
536+
PyAPI_FUNC(int) PyUnstable_Object_EnableDeferredRefcount(PyObject *);

Lib/test/test_capi/test_object.py

+46
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import enum
22
import unittest
3+
from test import support
34
from test.support import import_helper
45
from test.support import os_helper
6+
from test.support import threading_helper
57

68
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
79
_testcapi = import_helper.import_module('_testcapi')
10+
_testinternalcapi = import_helper.import_module('_testinternalcapi')
811

912

1013
class Constant(enum.IntEnum):
@@ -131,5 +134,48 @@ def test_ClearWeakRefsNoCallbacks_no_weakref_support(self):
131134
_testcapi.pyobject_clear_weakrefs_no_callbacks(obj)
132135

133136

137+
class EnableDeferredRefcountingTest(unittest.TestCase):
138+
"""Test PyUnstable_Object_EnableDeferredRefcount"""
139+
@support.requires_resource("cpu")
140+
def test_enable_deferred_refcount(self):
141+
from threading import Thread
142+
143+
self.assertEqual(_testcapi.pyobject_enable_deferred_refcount("not tracked"), 0)
144+
foo = []
145+
self.assertEqual(_testcapi.pyobject_enable_deferred_refcount(foo), int(support.Py_GIL_DISABLED))
146+
147+
# Make sure reference counting works on foo now
148+
self.assertEqual(foo, [])
149+
if support.Py_GIL_DISABLED:
150+
self.assertTrue(_testinternalcapi.has_deferred_refcount(foo))
151+
152+
# Make sure that PyUnstable_Object_EnableDeferredRefcount is thread safe
153+
def silly_func(obj):
154+
self.assertIn(
155+
_testcapi.pyobject_enable_deferred_refcount(obj),
156+
(0, 1)
157+
)
158+
159+
silly_list = [1, 2, 3]
160+
threads = [
161+
Thread(target=silly_func, args=(silly_list,)) for _ in range(5)
162+
]
163+
164+
with threading_helper.catch_threading_exception() as cm:
165+
for t in threads:
166+
t.start()
167+
168+
for i in range(10):
169+
silly_list.append(i)
170+
171+
for t in threads:
172+
t.join()
173+
174+
self.assertIsNone(cm.exc_value)
175+
176+
if support.Py_GIL_DISABLED:
177+
self.assertTrue(_testinternalcapi.has_deferred_refcount(silly_list))
178+
179+
134180
if __name__ == "__main__":
135181
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added the :c:func:`PyUnstable_Object_EnableDeferredRefcount` function for
2+
enabling :pep:`703` deferred reference counting.

Modules/_testcapi/object.c

+8-1
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,20 @@ pyobject_clear_weakrefs_no_callbacks(PyObject *self, PyObject *obj)
124124
Py_RETURN_NONE;
125125
}
126126

127+
static PyObject *
128+
pyobject_enable_deferred_refcount(PyObject *self, PyObject *obj)
129+
{
130+
int result = PyUnstable_Object_EnableDeferredRefcount(obj);
131+
return PyLong_FromLong(result);
132+
}
133+
127134
static PyMethodDef test_methods[] = {
128135
{"call_pyobject_print", call_pyobject_print, METH_VARARGS},
129136
{"pyobject_print_null", pyobject_print_null, METH_VARARGS},
130137
{"pyobject_print_noref_object", pyobject_print_noref_object, METH_VARARGS},
131138
{"pyobject_print_os_error", pyobject_print_os_error, METH_VARARGS},
132139
{"pyobject_clear_weakrefs_no_callbacks", pyobject_clear_weakrefs_no_callbacks, METH_O},
133-
140+
{"pyobject_enable_deferred_refcount", pyobject_enable_deferred_refcount, METH_O},
134141
{NULL},
135142
};
136143

Modules/_testinternalcapi.c

+9
Original file line numberDiff line numberDiff line change
@@ -2069,6 +2069,14 @@ identify_type_slot_wrappers(PyObject *self, PyObject *Py_UNUSED(ignored))
20692069
return _PyType_GetSlotWrapperNames();
20702070
}
20712071

2072+
2073+
static PyObject *
2074+
has_deferred_refcount(PyObject *self, PyObject *op)
2075+
{
2076+
return PyBool_FromLong(_PyObject_HasDeferredRefcount(op));
2077+
}
2078+
2079+
20722080
static PyMethodDef module_functions[] = {
20732081
{"get_configs", get_configs, METH_NOARGS},
20742082
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2165,6 +2173,7 @@ static PyMethodDef module_functions[] = {
21652173
GH_119213_GETARGS_METHODDEF
21662174
{"get_static_builtin_types", get_static_builtin_types, METH_NOARGS},
21672175
{"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS},
2176+
{"has_deferred_refcount", has_deferred_refcount, METH_O},
21682177
{NULL, NULL} /* sentinel */
21692178
};
21702179

Objects/object.c

+29
Original file line numberDiff line numberDiff line change
@@ -2519,6 +2519,35 @@ _PyObject_SetDeferredRefcount(PyObject *op)
25192519
#endif
25202520
}
25212521

2522+
int
2523+
PyUnstable_Object_EnableDeferredRefcount(PyObject *op)
2524+
{
2525+
#ifdef Py_GIL_DISABLED
2526+
if (!PyType_IS_GC(Py_TYPE(op))) {
2527+
// Deferred reference counting doesn't work
2528+
// on untracked types.
2529+
return 0;
2530+
}
2531+
2532+
uint8_t bits = _Py_atomic_load_uint8(&op->ob_gc_bits);
2533+
if ((bits & _PyGC_BITS_DEFERRED) != 0)
2534+
{
2535+
// Nothing to do.
2536+
return 0;
2537+
}
2538+
2539+
if (_Py_atomic_compare_exchange_uint8(&op->ob_gc_bits, &bits, bits | _PyGC_BITS_DEFERRED) == 0)
2540+
{
2541+
// Someone beat us to it!
2542+
return 0;
2543+
}
2544+
_Py_atomic_add_ssize(&op->ob_ref_shared, _Py_REF_SHARED(_Py_REF_DEFERRED, 0));
2545+
return 1;
2546+
#else
2547+
return 0;
2548+
#endif
2549+
}
2550+
25222551
void
25232552
_Py_ResurrectReference(PyObject *op)
25242553
{

0 commit comments

Comments
 (0)