]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-139103: Use borrowed references for positional args in _PyStack_UnpackDict (gh...
authorSam Gross <colesbury@gmail.com>
Tue, 3 Feb 2026 17:24:35 +0000 (12:24 -0500)
committerGitHub <noreply@github.com>
Tue, 3 Feb 2026 17:24:35 +0000 (12:24 -0500)
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
Python/ceval.c

index af42fc8f7f2dbf2f262d272e15734f359d5d1998..4b1b4bd52a2e5683351797e1e0f3d96ecd82091b 100644 (file)
@@ -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);
 }
index c59f20bbf1e803a1d6d6ab061877a2da65036e2d..590b315ab65c2c034de25b61cf4b124e1684f8f3 100644 (file)
@@ -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 {