From e02137ea4e6ca3f12c5923782b4b789566f80dac Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 26 May 2025 19:31:04 -0400 Subject: [PATCH 1/6] Use deferred reference counting for threading primitives. --- Lib/threading.py | 9 +++++++++ Modules/_threadmodule.c | 2 ++ Python/clinic/sysmodule.c.h | 32 +++++++++++++++++++++++++++++++- Python/sysmodule.c | 19 +++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/Lib/threading.py b/Lib/threading.py index b6c451d1fbaabd..4ca067a130e101 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -73,6 +73,11 @@ _profile_hook = None _trace_hook = None +def _defer_refcount(op): + """Improve multithreaded scaling on the free-threade build.""" + if hasattr(_sys, "_defer_refcount"): + _sys._defer_refcount(op) + def setprofile(func): """Set a profile function for all threads started from the threading module. @@ -298,6 +303,7 @@ def __init__(self, lock=None): if hasattr(lock, '_is_owned'): self._is_owned = lock._is_owned self._waiters = _deque() + _defer_refcount(self) def _at_fork_reinit(self): self._lock._at_fork_reinit() @@ -466,6 +472,7 @@ def __init__(self, value=1): raise ValueError("semaphore initial value must be >= 0") self._cond = Condition(Lock()) self._value = value + _defer_refcount(self) def __repr__(self): cls = self.__class__ @@ -595,6 +602,7 @@ class Event: def __init__(self): self._cond = Condition(Lock()) self._flag = False + _defer_refcount(self) def __repr__(self): cls = self.__class__ @@ -700,6 +708,7 @@ def __init__(self, parties, action=None, timeout=None): self._parties = parties self._state = 0 # 0 filling, 1 draining, -1 resetting, -2 broken self._count = 0 + _defer_refcount(self) def __repr__(self): cls = self.__class__ diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index cc83be4b5ff311..8669d1bb2f6387 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -950,6 +950,7 @@ lock_new_impl(PyTypeObject *type) if (self == NULL) { return NULL; } + _PyObject_SetDeferredRefcount((PyObject *)self); self->lock = (PyMutex){0}; return (PyObject *)self; } @@ -1221,6 +1222,7 @@ rlock_new_impl(PyTypeObject *type) if (self == NULL) { return NULL; } + _PyObject_SetDeferredRefcount((PyObject *)self); self->lock = (_PyRecursiveMutex){0}; return (PyObject *) self; } diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index a47e4d11b54441..5c69acf3dfacb5 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -1821,6 +1821,36 @@ sys__is_gil_enabled(PyObject *module, PyObject *Py_UNUSED(ignored)) return return_value; } +PyDoc_STRVAR(sys__defer_refcount__doc__, +"_defer_refcount($module, op, /)\n" +"--\n" +"\n" +"Defer reference counting for the object, allowing for better scaling across multiple threads.\n" +"\n" +"This function should be used for specialized purposes only."); + +#define SYS__DEFER_REFCOUNT_METHODDEF \ + {"_defer_refcount", (PyCFunction)sys__defer_refcount, METH_O, sys__defer_refcount__doc__}, + +static int +sys__defer_refcount_impl(PyObject *module, PyObject *op); + +static PyObject * +sys__defer_refcount(PyObject *module, PyObject *op) +{ + PyObject *return_value = NULL; + int _return_value; + + _return_value = sys__defer_refcount_impl(module, op); + if ((_return_value == -1) && PyErr_Occurred()) { + goto exit; + } + return_value = PyBool_FromLong((long)_return_value); + +exit: + return return_value; +} + PyDoc_STRVAR(_jit_is_available__doc__, "is_available($module, /)\n" "--\n" @@ -1948,4 +1978,4 @@ _jit_is_active(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=449d16326e69dcf6 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e84ea46c0ecf9fa3 input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 4ed045e3297bbc..ab764eced723e0 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -15,6 +15,7 @@ Data members: */ #include "Python.h" +#include "object.h" #include "pycore_audit.h" // _Py_AuditHookEntry #include "pycore_call.h" // _PyObject_CallNoArgs() #include "pycore_ceval.h" // _PyEval_SetAsyncGenFinalizer() @@ -2653,6 +2654,23 @@ sys__is_gil_enabled_impl(PyObject *module) #endif } +/*[clinic input] +sys._defer_refcount -> bool + + op: object + / + +Defer reference counting for the object, allowing for better scaling across multiple threads. + +This function should be used for specialized purposes only. +[clinic start generated code]*/ + +static int +sys__defer_refcount_impl(PyObject *module, PyObject *op) +/*[clinic end generated code: output=3b965122056085f5 input=a081971a76c49e64]*/ +{ + return PyUnstable_Object_EnableDeferredRefcount(op); +} #ifndef MS_WINDOWS static PerfMapState perf_map_state; @@ -2834,6 +2852,7 @@ static PyMethodDef sys_methods[] = { SYS__GET_CPU_COUNT_CONFIG_METHODDEF SYS__IS_GIL_ENABLED_METHODDEF SYS__DUMP_TRACELETS_METHODDEF + SYS__DEFER_REFCOUNT_METHODDEF {NULL, NULL} // sentinel }; From 6093acb1bd747e4e6bd6e067828bc1a2d44c05ac Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 26 May 2025 19:40:43 -0400 Subject: [PATCH 2/6] Document and test sys._defer_refcount() --- Doc/library/sys.rst | 19 +++++++++++++++++++ Lib/test/test_sys.py | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 55e442b20ff877..0bcbb3defd552c 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -290,6 +290,25 @@ always available. Unless explicitly noted otherwise, all variables are read-only This function is specific to CPython. The exact output format is not defined here, and may change. +.. function:: _defer_refcount(op) + + Enable deferred reference counting on *op*, mitigating reference count + contention on :term:`free threaded ` builds of Python. + + Return :const:`True` if deferred reference counting was enabled on *op*, + and :const:`False` otherwise. + + .. versionadded:: next + + .. impl-detail:: + + This function should be used for specialized purposes only. + It is not guaranteed to exist in all implementations of Python. + + .. seealso:: + + :c:func:`PyUnstable_Object_EnableDeferredRefcount` + .. data:: dllhandle diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 795d1ecbb59f8f..c6d5a788b07253 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1343,6 +1343,25 @@ def test_pystats(self): def test_disable_gil_abi(self): self.assertEqual('t' in sys.abiflags, support.Py_GIL_DISABLED) + @test.support.cpython_only + @unittest.skipUnless(hasattr(sys, '_defer_refcount'), "requires _defer_refcount()") + def test_defer_refcount(self): + _testinternalcapi = import_helper.import_module('_testinternalcapi') + + class Test: + pass + + ref = Test() + if support.Py_GIL_DISABLED: + self.assertTrue(sys._defer_refcount(ref)) + self.assertTrue(_testinternalcapi.has_deferred_refcount(ref)) + self.assertFalse(sys._defer_refcount(ref)) + self.assertFalse(sys._defer_refcount(42)) + else: + self.assertFalse(sys._defer_refcount(ref)) + self.assertFalse(_testinternalcapi.has_deferred_refcount(ref)) + self.assertFalse(sys._defer_refcount(42)) + @test.support.cpython_only class UnraisableHookTest(unittest.TestCase): From 478d482b1b5b31ff754e2162a0d72feab81b3267 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 26 May 2025 19:49:23 -0400 Subject: [PATCH 3/6] Add blurb. --- .../next/Library/2025-05-26-19-49-16.gh-issue-134761.LeMefI.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-26-19-49-16.gh-issue-134761.LeMefI.rst diff --git a/Misc/NEWS.d/next/Library/2025-05-26-19-49-16.gh-issue-134761.LeMefI.rst b/Misc/NEWS.d/next/Library/2025-05-26-19-49-16.gh-issue-134761.LeMefI.rst new file mode 100644 index 00000000000000..b46c44e01da75f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-26-19-49-16.gh-issue-134761.LeMefI.rst @@ -0,0 +1,2 @@ +Improve performance when using :mod:`threading` primitives across multiple +threads. From f0fc73c22e47d2413025a83202559d9b3813364e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 26 May 2025 20:23:27 -0400 Subject: [PATCH 4/6] Remove sys._defer_refcount() documentation. --- Doc/library/sys.rst | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 0bcbb3defd552c..bd2433399851d5 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -290,26 +290,6 @@ always available. Unless explicitly noted otherwise, all variables are read-only This function is specific to CPython. The exact output format is not defined here, and may change. -.. function:: _defer_refcount(op) - - Enable deferred reference counting on *op*, mitigating reference count - contention on :term:`free threaded ` builds of Python. - - Return :const:`True` if deferred reference counting was enabled on *op*, - and :const:`False` otherwise. - - .. versionadded:: next - - .. impl-detail:: - - This function should be used for specialized purposes only. - It is not guaranteed to exist in all implementations of Python. - - .. seealso:: - - :c:func:`PyUnstable_Object_EnableDeferredRefcount` - - .. data:: dllhandle Integer specifying the handle of the Python DLL. From d407ad9122a098ee3cb850e3abb32dbaedbeb65b Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 26 May 2025 20:23:47 -0400 Subject: [PATCH 5/6] Remove stray newline change. --- Doc/library/sys.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index bd2433399851d5..55e442b20ff877 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -290,6 +290,7 @@ always available. Unless explicitly noted otherwise, all variables are read-only This function is specific to CPython. The exact output format is not defined here, and may change. + .. data:: dllhandle Integer specifying the handle of the Python DLL. From c750d922facb92c45f54ea2bd47b94639545cfd8 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 27 May 2025 06:25:38 -0400 Subject: [PATCH 6/6] Move call to _PyObject_SetDeferredRefcount() --- Modules/_threadmodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 8669d1bb2f6387..3f3a38d5464e32 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -950,8 +950,8 @@ lock_new_impl(PyTypeObject *type) if (self == NULL) { return NULL; } - _PyObject_SetDeferredRefcount((PyObject *)self); self->lock = (PyMutex){0}; + _PyObject_SetDeferredRefcount((PyObject *)self); return (PyObject *)self; } @@ -1222,8 +1222,8 @@ rlock_new_impl(PyTypeObject *type) if (self == NULL) { return NULL; } - _PyObject_SetDeferredRefcount((PyObject *)self); self->lock = (_PyRecursiveMutex){0}; + _PyObject_SetDeferredRefcount((PyObject *)self); return (PyObject *) self; }