Skip to content

Commit d891b2b

Browse files
colesburyvstinner
andauthored
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 <vstinner@python.org> * Apply suggestion from @vstinner Co-authored-by: Victor Stinner <vstinner@python.org> * format * Update Include/internal/pycore_function.h Co-authored-by: Victor Stinner <vstinner@python.org> --------- Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 638d22c commit d891b2b

File tree

12 files changed

+214
-66
lines changed

12 files changed

+214
-66
lines changed

Include/internal/pycore_function.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ static inline PyObject* _PyFunction_GET_BUILTINS(PyObject *func) {
4747
#define _PyFunction_GET_BUILTINS(func) _PyFunction_GET_BUILTINS(_PyObject_CAST(func))
4848

4949

50+
/* Get the callable wrapped by a staticmethod.
51+
Returns a borrowed reference.
52+
The caller must ensure 'sm' is a staticmethod object. */
53+
extern PyObject *_PyStaticMethod_GetFunc(PyObject *sm);
54+
55+
5056
#ifdef __cplusplus
5157
}
5258
#endif

Include/internal/pycore_object.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,10 @@ _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef
898898
PyAPI_FUNC(int) _PyObject_GetMethodStackRef(PyThreadState *ts, PyObject *obj,
899899
PyObject *name, _PyStackRef *method);
900900

901+
// Like PyObject_GetAttr but returns a _PyStackRef. For types, this can
902+
// return a deferred reference to reduce reference count contention.
903+
PyAPI_FUNC(_PyStackRef) _PyObject_GetAttrStackRef(PyObject *obj, PyObject *name);
904+
901905
// Cache the provided init method in the specialization cache of type if the
902906
// provided type version matches the current version of the type.
903907
//

Include/internal/pycore_typeobject.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ extern "C" {
1010

1111
#include "pycore_interp_structs.h" // managed_static_type_state
1212
#include "pycore_moduleobject.h" // PyModuleObject
13+
#include "pycore_structs.h" // _PyStackRef
1314

1415

1516
/* state */
@@ -112,6 +113,8 @@ _PyType_IsReady(PyTypeObject *type)
112113
extern PyObject* _Py_type_getattro_impl(PyTypeObject *type, PyObject *name,
113114
int *suppress_missing_attribute);
114115
extern PyObject* _Py_type_getattro(PyObject *type, PyObject *name);
116+
extern _PyStackRef _Py_type_getattro_stackref(PyTypeObject *type, PyObject *name,
117+
int *suppress_missing_attribute);
115118

116119
extern PyObject* _Py_BaseObject_RichCompare(PyObject* self, PyObject* other, int op);
117120

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve scaling of :func:`~collections.namedtuple` instantiation in the
2+
free-threaded build.

Modules/_testinternalcapi/test_cases.c.h

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Objects/funcobject.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "pycore_long.h" // _PyLong_GetOne()
88
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
99
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
10+
#include "pycore_object_deferred.h" // _PyObject_SetDeferredRefcount()
1011
#include "pycore_pyerrors.h" // _PyErr_Occurred()
1112
#include "pycore_setobject.h" // _PySet_NextEntry()
1213
#include "pycore_stats.h"
@@ -1760,6 +1761,7 @@ sm_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
17601761
if (sm == NULL) {
17611762
return NULL;
17621763
}
1764+
_PyObject_SetDeferredRefcount((PyObject *)sm);
17631765
if (sm_set_callable(sm, callable) < 0) {
17641766
Py_DECREF(sm);
17651767
return NULL;
@@ -1926,9 +1928,17 @@ PyStaticMethod_New(PyObject *callable)
19261928
if (sm == NULL) {
19271929
return NULL;
19281930
}
1931+
_PyObject_SetDeferredRefcount((PyObject *)sm);
19291932
if (sm_set_callable(sm, callable) < 0) {
19301933
Py_DECREF(sm);
19311934
return NULL;
19321935
}
19331936
return (PyObject *)sm;
19341937
}
1938+
1939+
PyObject *
1940+
_PyStaticMethod_GetFunc(PyObject *self)
1941+
{
1942+
staticmethod *sm = _PyStaticMethod_CAST(self);
1943+
return sm->sm_callable;
1944+
}

Objects/object.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "pycore_tuple.h" // _PyTuple_DebugMallocStats()
3232
#include "pycore_typeobject.h" // _PyBufferWrapper_Type
3333
#include "pycore_typevarobject.h" // _PyTypeAlias_Type
34+
#include "pycore_stackref.h" // PyStackRef_FromPyObjectSteal
3435
#include "pycore_unionobject.h" // _PyUnion_Type
3536

3637

@@ -1334,6 +1335,54 @@ PyObject_GetAttr(PyObject *v, PyObject *name)
13341335
return result;
13351336
}
13361337

1338+
/* Like PyObject_GetAttr but returns a _PyStackRef.
1339+
For types (tp_getattro == _Py_type_getattro), this can return
1340+
a deferred reference to reduce reference count contention. */
1341+
_PyStackRef
1342+
_PyObject_GetAttrStackRef(PyObject *v, PyObject *name)
1343+
{
1344+
PyTypeObject *tp = Py_TYPE(v);
1345+
if (!PyUnicode_Check(name)) {
1346+
PyErr_Format(PyExc_TypeError,
1347+
"attribute name must be string, not '%.200s'",
1348+
Py_TYPE(name)->tp_name);
1349+
return PyStackRef_NULL;
1350+
}
1351+
1352+
/* Fast path for types - can return deferred references */
1353+
if (tp->tp_getattro == _Py_type_getattro) {
1354+
_PyStackRef result = _Py_type_getattro_stackref((PyTypeObject *)v, name, NULL);
1355+
if (PyStackRef_IsNull(result)) {
1356+
_PyObject_SetAttributeErrorContext(v, name);
1357+
}
1358+
return result;
1359+
}
1360+
1361+
/* Fall back to regular PyObject_GetAttr and convert to stackref */
1362+
PyObject *result = NULL;
1363+
if (tp->tp_getattro != NULL) {
1364+
result = (*tp->tp_getattro)(v, name);
1365+
}
1366+
else if (tp->tp_getattr != NULL) {
1367+
const char *name_str = PyUnicode_AsUTF8(name);
1368+
if (name_str == NULL) {
1369+
return PyStackRef_NULL;
1370+
}
1371+
result = (*tp->tp_getattr)(v, (char *)name_str);
1372+
}
1373+
else {
1374+
PyErr_Format(PyExc_AttributeError,
1375+
"'%.100s' object has no attribute '%U'",
1376+
tp->tp_name, name);
1377+
}
1378+
1379+
if (result == NULL) {
1380+
_PyObject_SetAttributeErrorContext(v, name);
1381+
return PyStackRef_NULL;
1382+
}
1383+
return PyStackRef_FromPyObjectSteal(result);
1384+
}
1385+
13371386
int
13381387
PyObject_GetOptionalAttr(PyObject *v, PyObject *name, PyObject **result)
13391388
{

Objects/typeobject.c

Lines changed: 103 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6375,102 +6375,153 @@ _PyType_SetFlagsRecursive(PyTypeObject *self, unsigned long mask, unsigned long
63756375
63766376
*/
63776377
PyObject *
6378-
_Py_type_getattro_impl(PyTypeObject *type, PyObject *name, int * suppress_missing_attribute)
6378+
_Py_type_getattro_impl(PyTypeObject *type, PyObject *name, int *suppress_missing_attribute)
6379+
{
6380+
_PyStackRef ref = _Py_type_getattro_stackref(type, name, suppress_missing_attribute);
6381+
if (PyStackRef_IsNull(ref)) {
6382+
return NULL;
6383+
}
6384+
return PyStackRef_AsPyObjectSteal(ref);
6385+
}
6386+
6387+
/* This is similar to PyObject_GenericGetAttr(),
6388+
but uses _PyType_LookupRef() instead of just looking in type->tp_dict. */
6389+
PyObject *
6390+
_Py_type_getattro(PyObject *tp, PyObject *name)
6391+
{
6392+
PyTypeObject *type = PyTypeObject_CAST(tp);
6393+
return _Py_type_getattro_impl(type, name, NULL);
6394+
}
6395+
6396+
/* Like _Py_type_getattro but returns a _PyStackRef.
6397+
This can return a deferred reference in the free-threaded build
6398+
when the attribute is found without going through a descriptor.
6399+
6400+
suppress_missing_attribute (optional):
6401+
* NULL: do not suppress the exception
6402+
* Non-zero pointer: suppress the PyExc_AttributeError and
6403+
set *suppress_missing_attribute to 1 to signal we are returning NULL while
6404+
having suppressed the exception (other exceptions are not suppressed)
6405+
*/
6406+
_PyStackRef
6407+
_Py_type_getattro_stackref(PyTypeObject *type, PyObject *name,
6408+
int *suppress_missing_attribute)
63796409
{
63806410
PyTypeObject *metatype = Py_TYPE(type);
6381-
PyObject *meta_attribute, *attribute;
6382-
descrgetfunc meta_get;
6383-
PyObject* res;
6411+
descrgetfunc meta_get = NULL;
63846412

63856413
if (!PyUnicode_Check(name)) {
63866414
PyErr_Format(PyExc_TypeError,
63876415
"attribute name must be string, not '%.200s'",
63886416
Py_TYPE(name)->tp_name);
6389-
return NULL;
6417+
return PyStackRef_NULL;
63906418
}
63916419

63926420
/* Initialize this type (we'll assume the metatype is initialized) */
63936421
if (!_PyType_IsReady(type)) {
63946422
if (PyType_Ready(type) < 0)
6395-
return NULL;
6423+
return PyStackRef_NULL;
63966424
}
63976425

6398-
/* No readable descriptor found yet */
6399-
meta_get = NULL;
6426+
/* Set up GC-visible stack refs */
6427+
_PyCStackRef result_ref, meta_attribute_ref, attribute_ref;
6428+
PyThreadState *tstate = _PyThreadState_GET();
6429+
_PyThreadState_PushCStackRef(tstate, &result_ref);
6430+
_PyThreadState_PushCStackRef(tstate, &meta_attribute_ref);
6431+
_PyThreadState_PushCStackRef(tstate, &attribute_ref);
64006432

64016433
/* Look for the attribute in the metatype */
6402-
meta_attribute = _PyType_LookupRef(metatype, name);
6434+
_PyType_LookupStackRefAndVersion(metatype, name, &meta_attribute_ref.ref);
64036435

6404-
if (meta_attribute != NULL) {
6405-
meta_get = Py_TYPE(meta_attribute)->tp_descr_get;
6436+
if (!PyStackRef_IsNull(meta_attribute_ref.ref)) {
6437+
PyObject *meta_attr_obj = PyStackRef_AsPyObjectBorrow(meta_attribute_ref.ref);
6438+
meta_get = Py_TYPE(meta_attr_obj)->tp_descr_get;
64066439

6407-
if (meta_get != NULL && PyDescr_IsData(meta_attribute)) {
6440+
if (meta_get != NULL && PyDescr_IsData(meta_attr_obj)) {
64086441
/* Data descriptors implement tp_descr_set to intercept
64096442
* writes. Assume the attribute is not overridden in
64106443
* type's tp_dict (and bases): call the descriptor now.
64116444
*/
6412-
res = meta_get(meta_attribute, (PyObject *)type,
6413-
(PyObject *)metatype);
6414-
Py_DECREF(meta_attribute);
6415-
return res;
6445+
PyObject *res = meta_get(meta_attr_obj, (PyObject *)type,
6446+
(PyObject *)metatype);
6447+
if (res != NULL) {
6448+
result_ref.ref = PyStackRef_FromPyObjectSteal(res);
6449+
}
6450+
goto done;
64166451
}
64176452
}
64186453

64196454
/* No data descriptor found on metatype. Look in tp_dict of this
64206455
* type and its bases */
6421-
attribute = _PyType_LookupRef(type, name);
6422-
if (attribute != NULL) {
6456+
_PyType_LookupStackRefAndVersion(type, name, &attribute_ref.ref);
6457+
if (!PyStackRef_IsNull(attribute_ref.ref)) {
64236458
/* Implement descriptor functionality, if any */
6424-
descrgetfunc local_get = Py_TYPE(attribute)->tp_descr_get;
6459+
PyObject *attr_obj = PyStackRef_AsPyObjectBorrow(attribute_ref.ref);
6460+
descrgetfunc local_get = Py_TYPE(attr_obj)->tp_descr_get;
64256461

6426-
Py_XDECREF(meta_attribute);
6462+
/* Release meta_attribute early since we found in local dict */
6463+
PyStackRef_CLEAR(meta_attribute_ref.ref);
64276464

64286465
if (local_get != NULL) {
6466+
/* Special case staticmethod to avoid descriptor call overhead.
6467+
* staticmethod.__get__ just returns the wrapped callable. */
6468+
if (Py_TYPE(attr_obj) == &PyStaticMethod_Type) {
6469+
PyObject *callable = _PyStaticMethod_GetFunc(attr_obj);
6470+
if (callable) {
6471+
result_ref.ref = PyStackRef_FromPyObjectNew(callable);
6472+
goto done;
6473+
}
6474+
}
64296475
/* NULL 2nd argument indicates the descriptor was
64306476
* found on the target object itself (or a base) */
6431-
res = local_get(attribute, (PyObject *)NULL,
6432-
(PyObject *)type);
6433-
Py_DECREF(attribute);
6434-
return res;
6477+
PyObject *res = local_get(attr_obj, (PyObject *)NULL,
6478+
(PyObject *)type);
6479+
if (res != NULL) {
6480+
result_ref.ref = PyStackRef_FromPyObjectSteal(res);
6481+
}
6482+
goto done;
64356483
}
64366484

6437-
return attribute;
6485+
/* No descriptor, return the attribute directly */
6486+
result_ref.ref = attribute_ref.ref;
6487+
attribute_ref.ref = PyStackRef_NULL;
6488+
goto done;
64386489
}
64396490

64406491
/* No attribute found in local __dict__ (or bases): use the
64416492
* descriptor from the metatype, if any */
64426493
if (meta_get != NULL) {
6443-
PyObject *res;
6444-
res = meta_get(meta_attribute, (PyObject *)type,
6445-
(PyObject *)metatype);
6446-
Py_DECREF(meta_attribute);
6447-
return res;
6494+
PyObject *meta_attr_obj = PyStackRef_AsPyObjectBorrow(meta_attribute_ref.ref);
6495+
PyObject *res = meta_get(meta_attr_obj, (PyObject *)type,
6496+
(PyObject *)metatype);
6497+
if (res != NULL) {
6498+
result_ref.ref = PyStackRef_FromPyObjectSteal(res);
6499+
}
6500+
goto done;
64486501
}
64496502

64506503
/* If an ordinary attribute was found on the metatype, return it now */
6451-
if (meta_attribute != NULL) {
6452-
return meta_attribute;
6504+
if (!PyStackRef_IsNull(meta_attribute_ref.ref)) {
6505+
result_ref.ref = meta_attribute_ref.ref;
6506+
meta_attribute_ref.ref = PyStackRef_NULL;
6507+
goto done;
64536508
}
64546509

64556510
/* Give up */
64566511
if (suppress_missing_attribute == NULL) {
64576512
PyErr_Format(PyExc_AttributeError,
6458-
"type object '%.100s' has no attribute '%U'",
6459-
type->tp_name, name);
6460-
} else {
6513+
"type object '%.100s' has no attribute '%U'",
6514+
type->tp_name, name);
6515+
}
6516+
else {
64616517
// signal the caller we have not set an PyExc_AttributeError and gave up
64626518
*suppress_missing_attribute = 1;
64636519
}
6464-
return NULL;
6465-
}
64666520

6467-
/* This is similar to PyObject_GenericGetAttr(),
6468-
but uses _PyType_LookupRef() instead of just looking in type->tp_dict. */
6469-
PyObject *
6470-
_Py_type_getattro(PyObject *tp, PyObject *name)
6471-
{
6472-
PyTypeObject *type = PyTypeObject_CAST(tp);
6473-
return _Py_type_getattro_impl(type, name, NULL);
6521+
done:
6522+
_PyThreadState_PopCStackRef(tstate, &attribute_ref);
6523+
_PyThreadState_PopCStackRef(tstate, &meta_attribute_ref);
6524+
return _PyThreadState_PopCStackRefSteal(tstate, &result_ref);
64746525
}
64756526

64766527
// Called by type_setattro(). Updates both the type dict and
@@ -10937,15 +10988,19 @@ static PyObject *
1093710988
slot_tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
1093810989
{
1093910990
PyThreadState *tstate = _PyThreadState_GET();
10940-
PyObject *func, *result;
10991+
PyObject *result;
1094110992

10942-
func = PyObject_GetAttr((PyObject *)type, &_Py_ID(__new__));
10943-
if (func == NULL) {
10993+
_PyCStackRef func_ref;
10994+
_PyThreadState_PushCStackRef(tstate, &func_ref);
10995+
func_ref.ref = _PyObject_GetAttrStackRef((PyObject *)type, &_Py_ID(__new__));
10996+
if (PyStackRef_IsNull(func_ref.ref)) {
10997+
_PyThreadState_PopCStackRef(tstate, &func_ref);
1094410998
return NULL;
1094510999
}
1094611000

11001+
PyObject *func = PyStackRef_AsPyObjectBorrow(func_ref.ref);
1094711002
result = _PyObject_Call_Prepend(tstate, func, (PyObject *)type, args, kwds);
10948-
Py_DECREF(func);
11003+
_PyThreadState_PopCStackRef(tstate, &func_ref);
1094911004
return result;
1095011005
}
1095111006

Python/bytecodes.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2392,10 +2392,9 @@ dummy_func(
23922392
}
23932393
else {
23942394
/* Classic, pushes one value. */
2395-
PyObject *attr_o = PyObject_GetAttr(PyStackRef_AsPyObjectBorrow(owner), name);
2395+
attr = _PyObject_GetAttrStackRef(PyStackRef_AsPyObjectBorrow(owner), name);
23962396
PyStackRef_CLOSE(owner);
2397-
ERROR_IF(attr_o == NULL);
2398-
attr = PyStackRef_FromPyObjectSteal(attr_o);
2397+
ERROR_IF(PyStackRef_IsNull(attr));
23992398
}
24002399
}
24012400

0 commit comments

Comments
 (0)