From 8529e2fbf5312e60ee2c48e492af0dd3c2113764 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 18 Feb 2026 17:03:11 -0500 Subject: [PATCH] gh-139103: Improve enum free-threaded scaling --- Lib/enum.py | 5 +++ ...-02-18-22-03-02.gh-issue-139103.ZOmb0m.rst | 2 ++ Objects/typeobject.c | 10 ++++-- Python/clinic/sysmodule.c.h | 34 ++++++++++++++++++- Python/sysmodule.c | 21 ++++++++++++ Tools/ftscalingbench/ftscalingbench.py | 20 +++++++++++ 6 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-18-22-03-02.gh-issue-139103.ZOmb0m.rst diff --git a/Lib/enum.py b/Lib/enum.py index 025e973446d88d..0311d75f584d12 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -185,6 +185,10 @@ class property(DynamicClassAttribute): _attr_type = None _cls_type = None + def __init__(self, fget=None, fset=None, fdel=None, doc=None): + sys._enable_deferred_refcount(self) + super().__init__(fget, fset, fdel, doc) + def __get__(self, instance, ownerclass=None): if instance is None: if self.member is not None: @@ -1067,6 +1071,7 @@ def _find_new_(mcls, classdict, member_type, first_enum): return __new__, save_new, use_args def _add_member_(cls, name, member): + sys._enable_deferred_refcount(member) # _value_ structures are not updated if name in cls._member_map_: if cls._member_map_[name] is not member: diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-18-22-03-02.gh-issue-139103.ZOmb0m.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-18-22-03-02.gh-issue-139103.ZOmb0m.rst new file mode 100644 index 00000000000000..02b398f49c7fe5 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-18-22-03-02.gh-issue-139103.ZOmb0m.rst @@ -0,0 +1,2 @@ +Add :func:`sys._enable_deferred_refcount` and use it in :mod:`enum` to +improve free-threaded scaling. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index ad26339c9c34df..1e0a18b86fa02f 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -10925,15 +10925,19 @@ static PyObject * slot_tp_descr_get(PyObject *self, PyObject *obj, PyObject *type) { PyTypeObject *tp = Py_TYPE(self); - PyObject *get; + PyThreadState *tstate = _PyThreadState_GET(); + _PyCStackRef cref; + _PyThreadState_PushCStackRef(tstate, &cref); - get = _PyType_LookupRef(tp, &_Py_ID(__get__)); + _PyType_LookupStackRefAndVersion(tp, &_Py_ID(__get__), &cref.ref); + PyObject *get = PyStackRef_AsPyObjectBorrow(cref.ref); if (get == NULL) { #ifndef Py_GIL_DISABLED /* Avoid further slowdowns */ if (tp->tp_descr_get == slot_tp_descr_get) tp->tp_descr_get = NULL; #endif + _PyThreadState_PopCStackRef(tstate, &cref); return Py_NewRef(self); } if (obj == NULL) @@ -10942,7 +10946,7 @@ slot_tp_descr_get(PyObject *self, PyObject *obj, PyObject *type) type = Py_None; PyObject *stack[3] = {self, obj, type}; PyObject *res = PyObject_Vectorcall(get, stack, 3, NULL); - Py_DECREF(get); + _PyThreadState_PopCStackRef(tstate, &cref); return res; } diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index f8ae7f18acc809..5c762ca5f18fc3 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -404,6 +404,38 @@ sys__is_immortal(PyObject *module, PyObject *op) return return_value; } +PyDoc_STRVAR(sys__enable_deferred_refcount__doc__, +"_enable_deferred_refcount($module, op, /)\n" +"--\n" +"\n" +"Enable deferred reference counting on the object.\n" +"\n" +"Return True if deferred reference counting was successfully enabled, and\n" +"False otherwise. This is primarily useful for avoiding reference count\n" +"contention on objects that are shared between multiple threads."); + +#define SYS__ENABLE_DEFERRED_REFCOUNT_METHODDEF \ + {"_enable_deferred_refcount", (PyCFunction)sys__enable_deferred_refcount, METH_O, sys__enable_deferred_refcount__doc__}, + +static int +sys__enable_deferred_refcount_impl(PyObject *module, PyObject *op); + +static PyObject * +sys__enable_deferred_refcount(PyObject *module, PyObject *op) +{ + PyObject *return_value = NULL; + int _return_value; + + _return_value = sys__enable_deferred_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(sys_settrace__doc__, "settrace($module, function, /)\n" "--\n" @@ -2121,4 +2153,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=adbadb629b98eabf input=a9049054013a1b77]*/ +/*[clinic end generated code: output=08032972fc990952 input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 28b2108940c853..a30700f8ea3fe5 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1052,6 +1052,26 @@ sys__is_immortal_impl(PyObject *module, PyObject *op) return PyUnstable_IsImmortal(op); } +/*[clinic input] +sys._enable_deferred_refcount -> bool + + op: object + / + +Enable deferred reference counting on the object. + +Return True if deferred reference counting was successfully enabled, and +False otherwise. This is primarily useful for avoiding reference count +contention on objects that are shared between multiple threads. +[clinic start generated code]*/ + +static int +sys__enable_deferred_refcount_impl(PyObject *module, PyObject *op) +/*[clinic end generated code: output=d19c0f74be9da2a8 input=92d197248dcfb1f7]*/ +{ + return PyUnstable_Object_EnableDeferredRefcount(op); +} + /* * Cached interned string objects used for calling the profile and * trace functions. @@ -2942,6 +2962,7 @@ static PyMethodDef sys_methods[] = { SYS_GETWINDOWSVERSION_METHODDEF SYS__ENABLELEGACYWINDOWSFSENCODING_METHODDEF SYS__IS_IMMORTAL_METHODDEF + SYS__ENABLE_DEFERRED_REFCOUNT_METHODDEF SYS_INTERN_METHODDEF SYS__IS_INTERNED_METHODDEF SYS_IS_FINALIZING_METHODDEF diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index 50d0e4c04fc319..94787000414249 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -30,6 +30,7 @@ import time from collections import namedtuple from dataclasses import dataclass +from enum import Enum from operator import methodcaller from typing import NamedTuple @@ -236,6 +237,25 @@ def instantiate_typing_namedtuple(): obj = MyTypingNamedTuple(x=1, y=2, z=3) +class MyEnum(Enum): + X = 1 + Y = 2 + Z = 3 + +@register_benchmark +def enum_attr(): + for _ in range(1000 * WORK_SCALE): + MyEnum.X + MyEnum.Y + MyEnum.Z + +@register_benchmark +def enum_value(): + for _ in range(1000 * WORK_SCALE): + MyEnum.X.value + MyEnum.Y.value + MyEnum.Z.value + @register_benchmark def deepcopy(): x = {'list': [1, 2], 'tuple': (1, None)}