https://github.com/python/cpython/commit/d891b2bbd16c25995df853121d2f134d3e357cd1 commit: d891b2bbd16c25995df853121d2f134d3e357cd1 branch: main author: Sam Gross <[email protected]> committer: colesbury <[email protected]> date: 2026-02-06T09:43:05-05:00 summary:
gh-139103: Improve namedtuple scaling in free-threaded build (gh-144332) Add `_Py_type_getattro_stackref`, a variant of type attribute lookup that returns `_PyStackRef` instead of `PyObject*`. This allows returning deferred references in the free-threaded build, reducing reference count contention when accessing type attributes. This significantly improves scaling of namedtuple instantiation across multiple threads. * Add blurb * Rename PyObject_GetAttrStackRef to _PyObject_GetAttrStackRef * Apply suggestion from @vstinner Co-authored-by: Victor Stinner <[email protected]> * Apply suggestion from @vstinner Co-authored-by: Victor Stinner <[email protected]> * format * Update Include/internal/pycore_function.h Co-authored-by: Victor Stinner <[email protected]> --------- Co-authored-by: Victor Stinner <[email protected]> files: A Misc/NEWS.d/next/Core_and_Builtins/2026-01-29-16-57-11.gh-issue-139103.icXIEQ.rst M Include/internal/pycore_function.h M Include/internal/pycore_object.h M Include/internal/pycore_typeobject.h M Modules/_testinternalcapi/test_cases.c.h M Objects/funcobject.c M Objects/object.c M Objects/typeobject.c M Python/bytecodes.c M Python/executor_cases.c.h M Python/generated_cases.c.h M Tools/ftscalingbench/ftscalingbench.py diff --git a/Include/internal/pycore_function.h b/Include/internal/pycore_function.h index e89f4b5c8a4ec1..522e03c6696993 100644 --- a/Include/internal/pycore_function.h +++ b/Include/internal/pycore_function.h @@ -47,6 +47,12 @@ static inline PyObject* _PyFunction_GET_BUILTINS(PyObject *func) { #define _PyFunction_GET_BUILTINS(func) _PyFunction_GET_BUILTINS(_PyObject_CAST(func)) +/* Get the callable wrapped by a staticmethod. + Returns a borrowed reference. + The caller must ensure 'sm' is a staticmethod object. */ +extern PyObject *_PyStaticMethod_GetFunc(PyObject *sm); + + #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index d14cee6af66103..8c241c7707d074 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -898,6 +898,10 @@ _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef PyAPI_FUNC(int) _PyObject_GetMethodStackRef(PyThreadState *ts, PyObject *obj, PyObject *name, _PyStackRef *method); +// Like PyObject_GetAttr but returns a _PyStackRef. For types, this can +// return a deferred reference to reduce reference count contention. +PyAPI_FUNC(_PyStackRef) _PyObject_GetAttrStackRef(PyObject *obj, PyObject *name); + // Cache the provided init method in the specialization cache of type if the // provided type version matches the current version of the type. // diff --git a/Include/internal/pycore_typeobject.h b/Include/internal/pycore_typeobject.h index abaa60890b55c8..dfd355d5012066 100644 --- a/Include/internal/pycore_typeobject.h +++ b/Include/internal/pycore_typeobject.h @@ -10,6 +10,7 @@ extern "C" { #include "pycore_interp_structs.h" // managed_static_type_state #include "pycore_moduleobject.h" // PyModuleObject +#include "pycore_structs.h" // _PyStackRef /* state */ @@ -112,6 +113,8 @@ _PyType_IsReady(PyTypeObject *type) extern PyObject* _Py_type_getattro_impl(PyTypeObject *type, PyObject *name, int *suppress_missing_attribute); extern PyObject* _Py_type_getattro(PyObject *type, PyObject *name); +extern _PyStackRef _Py_type_getattro_stackref(PyTypeObject *type, PyObject *name, + int *suppress_missing_attribute); extern PyObject* _Py_BaseObject_RichCompare(PyObject* self, PyObject* other, int op); diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-01-29-16-57-11.gh-issue-139103.icXIEQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-29-16-57-11.gh-issue-139103.icXIEQ.rst new file mode 100644 index 00000000000000..de3391dfcea708 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-29-16-57-11.gh-issue-139103.icXIEQ.rst @@ -0,0 +1,2 @@ +Improve scaling of :func:`~collections.namedtuple` instantiation in the +free-threaded build. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index c89c790988c52d..2a73a554eda2cc 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -7925,18 +7925,18 @@ } else { _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); + attr = _PyObject_GetAttrStackRef(PyStackRef_AsPyObjectBorrow(owner), name); stack_pointer = _PyFrame_GetStackPointer(frame); - stack_pointer += -1; + stack_pointer[-1] = attr; + stack_pointer += (oparg&1); ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); PyStackRef_CLOSE(owner); stack_pointer = _PyFrame_GetStackPointer(frame); - if (attr_o == NULL) { + if (PyStackRef_IsNull(attr)) { JUMP_TO_LABEL(error); } - attr = PyStackRef_FromPyObjectSteal(attr_o); - stack_pointer += 1; + stack_pointer += -(oparg&1); } } stack_pointer[-1] = attr; diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 2bf21fa045e3f1..8f4ff4e42392c2 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -7,6 +7,7 @@ #include "pycore_long.h" // _PyLong_GetOne() #include "pycore_modsupport.h" // _PyArg_NoKeywords() #include "pycore_object.h" // _PyObject_GC_UNTRACK() +#include "pycore_object_deferred.h" // _PyObject_SetDeferredRefcount() #include "pycore_pyerrors.h" // _PyErr_Occurred() #include "pycore_setobject.h" // _PySet_NextEntry() #include "pycore_stats.h" @@ -1760,6 +1761,7 @@ sm_new(PyTypeObject *type, PyObject *args, PyObject *kwds) if (sm == NULL) { return NULL; } + _PyObject_SetDeferredRefcount((PyObject *)sm); if (sm_set_callable(sm, callable) < 0) { Py_DECREF(sm); return NULL; @@ -1926,9 +1928,17 @@ PyStaticMethod_New(PyObject *callable) if (sm == NULL) { return NULL; } + _PyObject_SetDeferredRefcount((PyObject *)sm); if (sm_set_callable(sm, callable) < 0) { Py_DECREF(sm); return NULL; } return (PyObject *)sm; } + +PyObject * +_PyStaticMethod_GetFunc(PyObject *self) +{ + staticmethod *sm = _PyStaticMethod_CAST(self); + return sm->sm_callable; +} diff --git a/Objects/object.c b/Objects/object.c index 38717def24239f..a4f8ddf54b9484 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -31,6 +31,7 @@ #include "pycore_tuple.h" // _PyTuple_DebugMallocStats() #include "pycore_typeobject.h" // _PyBufferWrapper_Type #include "pycore_typevarobject.h" // _PyTypeAlias_Type +#include "pycore_stackref.h" // PyStackRef_FromPyObjectSteal #include "pycore_unionobject.h" // _PyUnion_Type @@ -1334,6 +1335,54 @@ PyObject_GetAttr(PyObject *v, PyObject *name) return result; } +/* Like PyObject_GetAttr but returns a _PyStackRef. + For types (tp_getattro == _Py_type_getattro), this can return + a deferred reference to reduce reference count contention. */ +_PyStackRef +_PyObject_GetAttrStackRef(PyObject *v, PyObject *name) +{ + PyTypeObject *tp = Py_TYPE(v); + if (!PyUnicode_Check(name)) { + PyErr_Format(PyExc_TypeError, + "attribute name must be string, not '%.200s'", + Py_TYPE(name)->tp_name); + return PyStackRef_NULL; + } + + /* Fast path for types - can return deferred references */ + if (tp->tp_getattro == _Py_type_getattro) { + _PyStackRef result = _Py_type_getattro_stackref((PyTypeObject *)v, name, NULL); + if (PyStackRef_IsNull(result)) { + _PyObject_SetAttributeErrorContext(v, name); + } + return result; + } + + /* Fall back to regular PyObject_GetAttr and convert to stackref */ + PyObject *result = NULL; + if (tp->tp_getattro != NULL) { + result = (*tp->tp_getattro)(v, name); + } + else if (tp->tp_getattr != NULL) { + const char *name_str = PyUnicode_AsUTF8(name); + if (name_str == NULL) { + return PyStackRef_NULL; + } + result = (*tp->tp_getattr)(v, (char *)name_str); + } + else { + PyErr_Format(PyExc_AttributeError, + "'%.100s' object has no attribute '%U'", + tp->tp_name, name); + } + + if (result == NULL) { + _PyObject_SetAttributeErrorContext(v, name); + return PyStackRef_NULL; + } + return PyStackRef_FromPyObjectSteal(result); +} + int PyObject_GetOptionalAttr(PyObject *v, PyObject *name, PyObject **result) { diff --git a/Objects/typeobject.c b/Objects/typeobject.c index ac52fe4002dc69..ad26339c9c34df 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6375,102 +6375,153 @@ _PyType_SetFlagsRecursive(PyTypeObject *self, unsigned long mask, unsigned long */ PyObject * -_Py_type_getattro_impl(PyTypeObject *type, PyObject *name, int * suppress_missing_attribute) +_Py_type_getattro_impl(PyTypeObject *type, PyObject *name, int *suppress_missing_attribute) +{ + _PyStackRef ref = _Py_type_getattro_stackref(type, name, suppress_missing_attribute); + if (PyStackRef_IsNull(ref)) { + return NULL; + } + return PyStackRef_AsPyObjectSteal(ref); +} + +/* This is similar to PyObject_GenericGetAttr(), + but uses _PyType_LookupRef() instead of just looking in type->tp_dict. */ +PyObject * +_Py_type_getattro(PyObject *tp, PyObject *name) +{ + PyTypeObject *type = PyTypeObject_CAST(tp); + return _Py_type_getattro_impl(type, name, NULL); +} + +/* Like _Py_type_getattro but returns a _PyStackRef. + This can return a deferred reference in the free-threaded build + when the attribute is found without going through a descriptor. + + suppress_missing_attribute (optional): + * NULL: do not suppress the exception + * Non-zero pointer: suppress the PyExc_AttributeError and + set *suppress_missing_attribute to 1 to signal we are returning NULL while + having suppressed the exception (other exceptions are not suppressed) +*/ +_PyStackRef +_Py_type_getattro_stackref(PyTypeObject *type, PyObject *name, + int *suppress_missing_attribute) { PyTypeObject *metatype = Py_TYPE(type); - PyObject *meta_attribute, *attribute; - descrgetfunc meta_get; - PyObject* res; + descrgetfunc meta_get = NULL; if (!PyUnicode_Check(name)) { PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'", Py_TYPE(name)->tp_name); - return NULL; + return PyStackRef_NULL; } /* Initialize this type (we'll assume the metatype is initialized) */ if (!_PyType_IsReady(type)) { if (PyType_Ready(type) < 0) - return NULL; + return PyStackRef_NULL; } - /* No readable descriptor found yet */ - meta_get = NULL; + /* Set up GC-visible stack refs */ + _PyCStackRef result_ref, meta_attribute_ref, attribute_ref; + PyThreadState *tstate = _PyThreadState_GET(); + _PyThreadState_PushCStackRef(tstate, &result_ref); + _PyThreadState_PushCStackRef(tstate, &meta_attribute_ref); + _PyThreadState_PushCStackRef(tstate, &attribute_ref); /* Look for the attribute in the metatype */ - meta_attribute = _PyType_LookupRef(metatype, name); + _PyType_LookupStackRefAndVersion(metatype, name, &meta_attribute_ref.ref); - if (meta_attribute != NULL) { - meta_get = Py_TYPE(meta_attribute)->tp_descr_get; + if (!PyStackRef_IsNull(meta_attribute_ref.ref)) { + PyObject *meta_attr_obj = PyStackRef_AsPyObjectBorrow(meta_attribute_ref.ref); + meta_get = Py_TYPE(meta_attr_obj)->tp_descr_get; - if (meta_get != NULL && PyDescr_IsData(meta_attribute)) { + if (meta_get != NULL && PyDescr_IsData(meta_attr_obj)) { /* Data descriptors implement tp_descr_set to intercept * writes. Assume the attribute is not overridden in * type's tp_dict (and bases): call the descriptor now. */ - res = meta_get(meta_attribute, (PyObject *)type, - (PyObject *)metatype); - Py_DECREF(meta_attribute); - return res; + PyObject *res = meta_get(meta_attr_obj, (PyObject *)type, + (PyObject *)metatype); + if (res != NULL) { + result_ref.ref = PyStackRef_FromPyObjectSteal(res); + } + goto done; } } /* No data descriptor found on metatype. Look in tp_dict of this * type and its bases */ - attribute = _PyType_LookupRef(type, name); - if (attribute != NULL) { + _PyType_LookupStackRefAndVersion(type, name, &attribute_ref.ref); + if (!PyStackRef_IsNull(attribute_ref.ref)) { /* Implement descriptor functionality, if any */ - descrgetfunc local_get = Py_TYPE(attribute)->tp_descr_get; + PyObject *attr_obj = PyStackRef_AsPyObjectBorrow(attribute_ref.ref); + descrgetfunc local_get = Py_TYPE(attr_obj)->tp_descr_get; - Py_XDECREF(meta_attribute); + /* Release meta_attribute early since we found in local dict */ + PyStackRef_CLEAR(meta_attribute_ref.ref); if (local_get != NULL) { + /* Special case staticmethod to avoid descriptor call overhead. + * staticmethod.__get__ just returns the wrapped callable. */ + if (Py_TYPE(attr_obj) == &PyStaticMethod_Type) { + PyObject *callable = _PyStaticMethod_GetFunc(attr_obj); + if (callable) { + result_ref.ref = PyStackRef_FromPyObjectNew(callable); + goto done; + } + } /* NULL 2nd argument indicates the descriptor was * found on the target object itself (or a base) */ - res = local_get(attribute, (PyObject *)NULL, - (PyObject *)type); - Py_DECREF(attribute); - return res; + PyObject *res = local_get(attr_obj, (PyObject *)NULL, + (PyObject *)type); + if (res != NULL) { + result_ref.ref = PyStackRef_FromPyObjectSteal(res); + } + goto done; } - return attribute; + /* No descriptor, return the attribute directly */ + result_ref.ref = attribute_ref.ref; + attribute_ref.ref = PyStackRef_NULL; + goto done; } /* No attribute found in local __dict__ (or bases): use the * descriptor from the metatype, if any */ if (meta_get != NULL) { - PyObject *res; - res = meta_get(meta_attribute, (PyObject *)type, - (PyObject *)metatype); - Py_DECREF(meta_attribute); - return res; + PyObject *meta_attr_obj = PyStackRef_AsPyObjectBorrow(meta_attribute_ref.ref); + PyObject *res = meta_get(meta_attr_obj, (PyObject *)type, + (PyObject *)metatype); + if (res != NULL) { + result_ref.ref = PyStackRef_FromPyObjectSteal(res); + } + goto done; } /* If an ordinary attribute was found on the metatype, return it now */ - if (meta_attribute != NULL) { - return meta_attribute; + if (!PyStackRef_IsNull(meta_attribute_ref.ref)) { + result_ref.ref = meta_attribute_ref.ref; + meta_attribute_ref.ref = PyStackRef_NULL; + goto done; } /* Give up */ if (suppress_missing_attribute == NULL) { PyErr_Format(PyExc_AttributeError, - "type object '%.100s' has no attribute '%U'", - type->tp_name, name); - } else { + "type object '%.100s' has no attribute '%U'", + type->tp_name, name); + } + else { // signal the caller we have not set an PyExc_AttributeError and gave up *suppress_missing_attribute = 1; } - return NULL; -} -/* This is similar to PyObject_GenericGetAttr(), - but uses _PyType_LookupRef() instead of just looking in type->tp_dict. */ -PyObject * -_Py_type_getattro(PyObject *tp, PyObject *name) -{ - PyTypeObject *type = PyTypeObject_CAST(tp); - return _Py_type_getattro_impl(type, name, NULL); +done: + _PyThreadState_PopCStackRef(tstate, &attribute_ref); + _PyThreadState_PopCStackRef(tstate, &meta_attribute_ref); + return _PyThreadState_PopCStackRefSteal(tstate, &result_ref); } // Called by type_setattro(). Updates both the type dict and @@ -10937,15 +10988,19 @@ static PyObject * slot_tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyThreadState *tstate = _PyThreadState_GET(); - PyObject *func, *result; + PyObject *result; - func = PyObject_GetAttr((PyObject *)type, &_Py_ID(__new__)); - if (func == NULL) { + _PyCStackRef func_ref; + _PyThreadState_PushCStackRef(tstate, &func_ref); + func_ref.ref = _PyObject_GetAttrStackRef((PyObject *)type, &_Py_ID(__new__)); + if (PyStackRef_IsNull(func_ref.ref)) { + _PyThreadState_PopCStackRef(tstate, &func_ref); return NULL; } + PyObject *func = PyStackRef_AsPyObjectBorrow(func_ref.ref); result = _PyObject_Call_Prepend(tstate, func, (PyObject *)type, args, kwds); - Py_DECREF(func); + _PyThreadState_PopCStackRef(tstate, &func_ref); return result; } diff --git a/Python/bytecodes.c b/Python/bytecodes.c index a014f56deb202e..818b4fbc3801c0 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2392,10 +2392,9 @@ dummy_func( } else { /* Classic, pushes one value. */ - PyObject *attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); + attr = _PyObject_GetAttrStackRef(PyStackRef_AsPyObjectBorrow(owner), name); PyStackRef_CLOSE(owner); - ERROR_IF(attr_o == NULL); - attr = PyStackRef_FromPyObjectSteal(attr_o); + ERROR_IF(PyStackRef_IsNull(attr)); } } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 08c547c4a0a3b4..a98ec2200485d2 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -8703,19 +8703,19 @@ stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); + attr = _PyObject_GetAttrStackRef(PyStackRef_AsPyObjectBorrow(owner), name); stack_pointer = _PyFrame_GetStackPointer(frame); - stack_pointer += -1; + stack_pointer[-1] = attr; + stack_pointer += (oparg&1); ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); PyStackRef_CLOSE(owner); stack_pointer = _PyFrame_GetStackPointer(frame); - if (attr_o == NULL) { + if (PyStackRef_IsNull(attr)) { SET_CURRENT_CACHED_VALUES(0); JUMP_TO_ERROR(); } - attr = PyStackRef_FromPyObjectSteal(attr_o); - stack_pointer += 1; + stack_pointer += -(oparg&1); } _tos_cache0 = PyStackRef_ZERO_BITS; _tos_cache1 = PyStackRef_ZERO_BITS; diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index be5dbfcc747935..fc1144a88d70cc 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -7924,18 +7924,18 @@ } else { _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name); + attr = _PyObject_GetAttrStackRef(PyStackRef_AsPyObjectBorrow(owner), name); stack_pointer = _PyFrame_GetStackPointer(frame); - stack_pointer += -1; + stack_pointer[-1] = attr; + stack_pointer += (oparg&1); ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); PyStackRef_CLOSE(owner); stack_pointer = _PyFrame_GetStackPointer(frame); - if (attr_o == NULL) { + if (PyStackRef_IsNull(attr)) { JUMP_TO_LABEL(error); } - attr = PyStackRef_FromPyObjectSteal(attr_o); - stack_pointer += 1; + stack_pointer += -(oparg&1); } } stack_pointer[-1] = attr; diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index c2bd7c3880bc90..50d0e4c04fc319 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -28,8 +28,10 @@ import sys import threading import time +from collections import namedtuple from dataclasses import dataclass from operator import methodcaller +from typing import NamedTuple # The iterations in individual benchmarks are scaled by this factor. WORK_SCALE = 100 @@ -215,6 +217,24 @@ def instantiate_dataclass(): for _ in range(1000 * WORK_SCALE): obj = MyDataClass(x=1, y=2, z=3) +MyNamedTuple = namedtuple("MyNamedTuple", ["x", "y", "z"]) + +@register_benchmark +def instantiate_namedtuple(): + for _ in range(1000 * WORK_SCALE): + obj = MyNamedTuple(x=1, y=2, z=3) + + +class MyTypingNamedTuple(NamedTuple): + x: int + y: int + z: int + +@register_benchmark +def instantiate_typing_namedtuple(): + for _ in range(1000 * WORK_SCALE): + obj = MyTypingNamedTuple(x=1, y=2, z=3) + @register_benchmark def deepcopy(): _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
