Skip to content

Commit 3600622

Browse files
committed
pythongh-139103: Improve namedtuple scaling in free-threaded build
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.
1 parent 0fa1fc6 commit 3600622

File tree

13 files changed

+247
-82
lines changed

13 files changed

+247
-82
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, or NULL if uninitialized.
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

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/call.c

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,10 @@ _PyStack_AsDict(PyObject *const *values, PyObject *kwnames)
935935
936936
The newly allocated argument vector supports PY_VECTORCALL_ARGUMENTS_OFFSET.
937937
938+
The positional arguments are borrowed references from the input array
939+
(which must be kept alive by the caller). The keyword argument values
940+
are new references.
941+
938942
When done, you must call _PyStack_UnpackDict_Free(stack, nargs, kwnames) */
939943
PyObject *const *
940944
_PyStack_UnpackDict(PyThreadState *tstate,
@@ -970,9 +974,9 @@ _PyStack_UnpackDict(PyThreadState *tstate,
970974

971975
stack++; /* For PY_VECTORCALL_ARGUMENTS_OFFSET */
972976

973-
/* Copy positional arguments */
977+
/* Copy positional arguments (borrowed references) */
974978
for (Py_ssize_t i = 0; i < nargs; i++) {
975-
stack[i] = Py_NewRef(args[i]);
979+
stack[i] = args[i];
976980
}
977981

978982
PyObject **kwstack = stack + nargs;
@@ -1009,9 +1013,10 @@ void
10091013
_PyStack_UnpackDict_Free(PyObject *const *stack, Py_ssize_t nargs,
10101014
PyObject *kwnames)
10111015
{
1012-
Py_ssize_t n = PyTuple_GET_SIZE(kwnames) + nargs;
1013-
for (Py_ssize_t i = 0; i < n; i++) {
1014-
Py_DECREF(stack[i]);
1016+
/* Only decref kwargs values, positional args are borrowed */
1017+
Py_ssize_t nkwargs = PyTuple_GET_SIZE(kwnames);
1018+
for (Py_ssize_t i = 0; i < nkwargs; i++) {
1019+
Py_DECREF(stack[nargs + i]);
10151020
}
10161021
_PyStack_UnpackDict_FreeNoDecRef(stack, kwnames);
10171022
}

Objects/funcobject.c

Lines changed: 9 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"
@@ -1868,7 +1869,15 @@ PyStaticMethod_New(PyObject *callable)
18681869
staticmethod *sm = (staticmethod *)
18691870
PyType_GenericAlloc(&PyStaticMethod_Type, 0);
18701871
if (sm != NULL) {
1872+
_PyObject_SetDeferredRefcount((PyObject *)sm);
18711873
sm->sm_callable = Py_NewRef(callable);
18721874
}
18731875
return (PyObject *)sm;
18741876
}
1877+
1878+
PyObject *
1879+
_PyStaticMethod_GetFunc(PyObject *self)
1880+
{
1881+
staticmethod *sm = _PyStaticMethod_CAST(self);
1882+
return sm->sm_callable;
1883+
}

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
{

0 commit comments

Comments
 (0)