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

gh-123619: Add an unstable C API function for enabling deferred reference counting #123635

Merged
merged 38 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7107103
Implement PyUnstable_Object_SetDeferredRefcount
ZeroIntensity Sep 3, 2024
3928ea6
Update documentation.
ZeroIntensity Sep 3, 2024
6d2d34e
Add docstring.
ZeroIntensity Sep 3, 2024
6af0adf
Add NEWS entry.
ZeroIntensity Sep 3, 2024
1cc3aae
Fix indentation.
ZeroIntensity Sep 3, 2024
fd087a5
Fix hyperlink reference.
ZeroIntensity Sep 3, 2024
2dacc0d
Update object.c
ZeroIntensity Sep 19, 2024
0c8b98d
Update object.rst
ZeroIntensity Sep 19, 2024
50ff96e
Rename to EnableDeferredRefcount
ZeroIntensity Sep 19, 2024
6be9286
No-op if the object already has DRC enabled
ZeroIntensity Sep 19, 2024
10f3a2d
Fix indentation.
ZeroIntensity Sep 19, 2024
78889d5
Use 1 for success.
ZeroIntensity Sep 19, 2024
fe2cb46
Remove redundant line.
ZeroIntensity Sep 19, 2024
7b95459
Add simple unit test.
ZeroIntensity Sep 19, 2024
042f610
Make thread-safe.
ZeroIntensity Oct 7, 2024
c1d9c74
Fix test.
ZeroIntensity Oct 7, 2024
a88aa31
Add owned-by-current-thread check back.
ZeroIntensity Oct 7, 2024
3f31693
Use atomic storage for ob_ref_shared
ZeroIntensity Oct 7, 2024
cedf106
No-op if the object is not a GC type.
ZeroIntensity Oct 21, 2024
b5f8df6
Update the documentation.
ZeroIntensity Oct 21, 2024
7e2653a
Update the docstring.
ZeroIntensity Oct 21, 2024
b831c77
Clarify thread safety in the documentation.
ZeroIntensity Oct 21, 2024
d0b5ed2
Fix thread safety on GC bits.
ZeroIntensity Oct 21, 2024
f2b2c78
Retain old reference count.
ZeroIntensity Oct 21, 2024
5728bc3
Require CPU resources for tests.
ZeroIntensity Nov 2, 2024
e6dd1f7
Fix typo.
ZeroIntensity Nov 2, 2024
85ce7dd
Add a what's new entry.
ZeroIntensity Nov 2, 2024
51a8a30
Merge branch 'main' into api-deferred-rc
ZeroIntensity Nov 2, 2024
1e4413f
Remove extra newline.
ZeroIntensity Nov 2, 2024
b57078c
Merge branch 'api-deferred-rc' of https://github.com/ZeroIntensity/cp…
ZeroIntensity Nov 2, 2024
99e9b64
Fix retainment of shared reference count.
ZeroIntensity Nov 2, 2024
d8e8af8
Update object.rst
ZeroIntensity Nov 4, 2024
7b3510d
Update object.rst
ZeroIntensity Nov 4, 2024
4c29547
Update Doc/c-api/object.rst
ZeroIntensity Nov 4, 2024
55443b6
Use _Py_atomic_add_ssize instead of store.
ZeroIntensity Nov 4, 2024
00debea
Merge branch 'api-deferred-rc' of https://github.com/ZeroIntensity/cp…
ZeroIntensity Nov 4, 2024
1a20d14
Don't load the GC bits more than once.
ZeroIntensity Nov 4, 2024
93e1030
Merge in the main branch
encukou Nov 13, 2024
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
24 changes: 24 additions & 0 deletions Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -575,3 +575,27 @@ Object Protocol
has the :c:macro:`Py_TPFLAGS_MANAGED_DICT` flag set.

.. versionadded:: 3.13

.. c:function:: int PyUnstable_Object_EnableDeferredRefcount(PyObject *obj)

Enable `deferred reference counting <https://peps.python.org/pep-0703/#deferred-reference-counting>`_ on *obj*,
if supported by the runtime. In the :term:`free-threaded <free threading>` build,
this allows the interpreter to avoid reference count adjustments to *obj*,
which may improve multi-threaded performance. The tradeoff is
that *obj* will only be deallocated by the tracing garbage collector.

This function returns ``1`` if deferred reference counting is enabled on *obj*
(including when it was enabled before the call),
and ``0`` if deferred reference counting is not supported or if the hint was
ignored by the runtime. This function is thread-safe, and cannot fail.

This function does nothing on builds with the :term:`GIL` enabled, which do
not support deferred reference counting. This also does nothing if *obj* is not
an object tracked by the garbage collector (see :func:`gc.is_tracked` and
:c:func:`PyObject_GC_IsTracked`).

This function is intended to be used soon after *obj* is created,
by the code that creates it.

.. versionadded:: next
ZeroIntensity marked this conversation as resolved.
Show resolved Hide resolved

3 changes: 3 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,9 @@ New features
* Add :c:func:`PyType_Freeze` function to make a type immutable.
(Contributed by Victor Stinner in :gh:`121654`.)

* Add :c:func:`PyUnstable_Object_EnableDeferredRefcount` for enabling
deferred reference counting, as outlined in :pep:`703`.

Porting to Python 3.14
----------------------

Expand Down
7 changes: 7 additions & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,10 @@ typedef enum {
typedef int (*PyRefTracer)(PyObject *, PyRefTracerEvent event, void *);
PyAPI_FUNC(int) PyRefTracer_SetTracer(PyRefTracer tracer, void *data);
PyAPI_FUNC(PyRefTracer) PyRefTracer_GetTracer(void**);

/* Enable PEP-703 deferred reference counting on the object.
*
* Returns 1 if deferred reference counting was successfully enabled, and
* 0 if the runtime ignored it. This function cannot fail.
*/
PyAPI_FUNC(int) PyUnstable_Object_EnableDeferredRefcount(PyObject *);
46 changes: 46 additions & 0 deletions Lib/test/test_capi/test_object.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import enum
import unittest
from test import support
from test.support import import_helper
from test.support import os_helper
from test.support import threading_helper

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


class Constant(enum.IntEnum):
Expand Down Expand Up @@ -131,5 +134,48 @@ def test_ClearWeakRefsNoCallbacks_no_weakref_support(self):
_testcapi.pyobject_clear_weakrefs_no_callbacks(obj)


class EnableDeferredRefcountingTest(unittest.TestCase):
"""Test PyUnstable_Object_EnableDeferredRefcount"""
@support.requires_resource("cpu")
def test_enable_deferred_refcount(self):
from threading import Thread

self.assertEqual(_testcapi.pyobject_enable_deferred_refcount("not tracked"), 0)
foo = []
self.assertEqual(_testcapi.pyobject_enable_deferred_refcount(foo), int(support.Py_GIL_DISABLED))

# Make sure reference counting works on foo now
self.assertEqual(foo, [])
if support.Py_GIL_DISABLED:
self.assertTrue(_testinternalcapi.has_deferred_refcount(foo))

# Make sure that PyUnstable_Object_EnableDeferredRefcount is thread safe
def silly_func(obj):
self.assertIn(
_testcapi.pyobject_enable_deferred_refcount(obj),
(0, 1)
)

silly_list = [1, 2, 3]
threads = [
Thread(target=silly_func, args=(silly_list,)) for _ in range(5)
]

with threading_helper.catch_threading_exception() as cm:
for t in threads:
t.start()

for i in range(10):
silly_list.append(i)

for t in threads:
t.join()

self.assertIsNone(cm.exc_value)

if support.Py_GIL_DISABLED:
self.assertTrue(_testinternalcapi.has_deferred_refcount(silly_list))


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added the :c:func:`PyUnstable_Object_EnableDeferredRefcount` function for
enabling :pep:`703` deferred reference counting.
9 changes: 8 additions & 1 deletion Modules/_testcapi/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,20 @@ pyobject_clear_weakrefs_no_callbacks(PyObject *self, PyObject *obj)
Py_RETURN_NONE;
}

static PyObject *
pyobject_enable_deferred_refcount(PyObject *self, PyObject *obj)
{
int result = PyUnstable_Object_EnableDeferredRefcount(obj);
return PyLong_FromLong(result);
}

static PyMethodDef test_methods[] = {
{"call_pyobject_print", call_pyobject_print, METH_VARARGS},
{"pyobject_print_null", pyobject_print_null, METH_VARARGS},
{"pyobject_print_noref_object", pyobject_print_noref_object, METH_VARARGS},
{"pyobject_print_os_error", pyobject_print_os_error, METH_VARARGS},
{"pyobject_clear_weakrefs_no_callbacks", pyobject_clear_weakrefs_no_callbacks, METH_O},

{"pyobject_enable_deferred_refcount", pyobject_enable_deferred_refcount, METH_O},
{NULL},
};

Expand Down
7 changes: 7 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -2023,6 +2023,12 @@ identify_type_slot_wrappers(PyObject *self, PyObject *Py_UNUSED(ignored))
}


static PyObject *
has_deferred_refcount(PyObject *self, PyObject *op)
{
return PyBool_FromLong(_PyObject_HasDeferredRefcount(op));
}

static PyMethodDef module_functions[] = {
{"get_configs", get_configs, METH_NOARGS},
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
Expand Down Expand Up @@ -2117,6 +2123,7 @@ static PyMethodDef module_functions[] = {
GH_119213_GETARGS_METHODDEF
{"get_static_builtin_types", get_static_builtin_types, METH_NOARGS},
{"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS},
{"has_deferred_refcount", has_deferred_refcount, METH_O},
{NULL, NULL} /* sentinel */
};

Expand Down
28 changes: 28 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -2519,6 +2519,34 @@ _PyObject_SetDeferredRefcount(PyObject *op)
#endif
}

int
PyUnstable_Object_EnableDeferredRefcount(PyObject *op)
{
#ifdef Py_GIL_DISABLED
if (!PyType_IS_GC(Py_TYPE(op))) {
// Deferred reference counting doesn't work
// on untracked types.
return 0;
}

if (_PyObject_HasDeferredRefcount(op)) {
// Nothing to do
return 0;
}

uint8_t bits = _Py_atomic_load_uint8(&op->ob_gc_bits);
if (_Py_atomic_compare_exchange_uint8(&op->ob_gc_bits, &bits, bits | _PyGC_BITS_DEFERRED) == 0)
{
// Someone beat us to it!
return 0;
}
colesbury marked this conversation as resolved.
Show resolved Hide resolved
_Py_atomic_add_ssize(&op->ob_ref_shared, _Py_REF_SHARED(_Py_REF_DEFERRED, 0));
return 1;
#else
return 0;
#endif
}

void
_Py_ResurrectReference(PyObject *op)
{
Expand Down
Loading