From 42d8d7ee671208ae0ef0ba9b1f33a477ee173ecf Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Mon, 2 Feb 2026 14:55:06 -0500 Subject: [PATCH] gh-139103: Use borrowed references for positional args in _PyStack_UnpackDict The positional arguments passed to _PyStack_UnpackDict are already kept alive by the caller, so we can avoid the extra reference count operations by using borrowed references instead of creating new ones. This reduces reference count contention in the free-threaded build when calling functions with keyword arguments. In particular, this avoids contention on the type argument to `__new__` when instantiating namedtuples with keyword arguments. --- Objects/call.c | 15 ++++++++++----- Python/ceval.c | 11 ++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Objects/call.c b/Objects/call.c index af42fc8f7f2dbf..4b1b4bd52a2e56 100644 --- a/Objects/call.c +++ b/Objects/call.c @@ -935,6 +935,10 @@ _PyStack_AsDict(PyObject *const *values, PyObject *kwnames) The newly allocated argument vector supports PY_VECTORCALL_ARGUMENTS_OFFSET. + The positional arguments are borrowed references from the input array + (which must be kept alive by the caller). The keyword argument values + are new references. + When done, you must call _PyStack_UnpackDict_Free(stack, nargs, kwnames) */ PyObject *const * _PyStack_UnpackDict(PyThreadState *tstate, @@ -970,9 +974,9 @@ _PyStack_UnpackDict(PyThreadState *tstate, stack++; /* For PY_VECTORCALL_ARGUMENTS_OFFSET */ - /* Copy positional arguments */ + /* Copy positional arguments (borrowed references) */ for (Py_ssize_t i = 0; i < nargs; i++) { - stack[i] = Py_NewRef(args[i]); + stack[i] = args[i]; } PyObject **kwstack = stack + nargs; @@ -1009,9 +1013,10 @@ void _PyStack_UnpackDict_Free(PyObject *const *stack, Py_ssize_t nargs, PyObject *kwnames) { - Py_ssize_t n = PyTuple_GET_SIZE(kwnames) + nargs; - for (Py_ssize_t i = 0; i < n; i++) { - Py_DECREF(stack[i]); + /* Only decref kwargs values, positional args are borrowed */ + Py_ssize_t nkwargs = PyTuple_GET_SIZE(kwnames); + for (Py_ssize_t i = 0; i < nkwargs; i++) { + Py_DECREF(stack[nargs + i]); } _PyStack_UnpackDict_FreeNoDecRef(stack, kwnames); } diff --git a/Python/ceval.c b/Python/ceval.c index c59f20bbf1e803..590b315ab65c2c 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2000,11 +2000,16 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, PyStackRef_CLOSE(func); goto error; } - size_t total_args = nargs + PyDict_GET_SIZE(kwargs); + size_t nkwargs = PyDict_GET_SIZE(kwargs); assert(sizeof(PyObject *) == sizeof(_PyStackRef)); newargs = (_PyStackRef *)object_array; - for (size_t i = 0; i < total_args; i++) { - newargs[i] = PyStackRef_FromPyObjectSteal(object_array[i]); + /* Positional args are borrowed from callargs tuple, need new reference */ + for (Py_ssize_t i = 0; i < nargs; i++) { + newargs[i] = PyStackRef_FromPyObjectNew(object_array[i]); + } + /* Keyword args are owned by _PyStack_UnpackDict, steal them */ + for (size_t i = 0; i < nkwargs; i++) { + newargs[nargs + i] = PyStackRef_FromPyObjectSteal(object_array[nargs + i]); } } else {