https://github.com/python/cpython/commit/79c43e7c249e61d959550c20f798a88c8829a8a8
commit: 79c43e7c249e61d959550c20f798a88c8829a8a8
branch: main
author: Sam Gross <[email protected]>
committer: colesbury <[email protected]>
date: 2026-02-03T12:24:35-05:00
summary:

gh-139103: Use borrowed references for positional args in _PyStack_UnpackDict 
(gh-144407)

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.

files:
M Objects/call.c
M Python/ceval.c

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 {

_______________________________________________
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]

Reply via email to