Skip to content

Conversation

@colesbury
Copy link
Contributor

@colesbury colesbury commented Feb 2, 2026

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.

…ack_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.
@colesbury colesbury changed the title gh-139103: Use borrowed references for positional args in _PyStack_Un… gh-139103: Use borrowed references for positional args in _PyStack_UnpackDict Feb 2, 2026
Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

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.

IMO it's a reasonable assumption. If there are bugs, the caller should be modified to hold a strong reference to its arguments.

_PyStack_UnpackDict() callers:

  • _PyObject_VectorcallDictTstate(): PyObject *const *args
  • _PyVectorcall_Call(): PyObject *tuple
  • _PyEvalFramePushAndInit_Ex(): PyObject *callargs (tuple)

_PyObject_VectorcallDictTstate() callers:

  • PyObject_VectorcallDict(): PyObject *const *args
  • PyEval_CallObjectWithKeywords(): pass NULL for args
  • _PyObject_Call_Prepend(): PyObject *args (tuple)

Most calls take their positional arguments from a tuple (safe). The most risky code path are calls to PyObject_VectorcallDict(): the change makes the assumption that this function is only called with strong references.

@colesbury
Copy link
Contributor Author

The most risky code path are calls to PyObject_VectorcallDict(): the change makes the assumption that this function is only called with strong references

FWIW, PyObject_VectorcallDict already implicitly has this assumption because we don't incref the args in the no-keyword case:

cpython/Objects/call.c

Lines 133 to 136 in 29acc08

PyObject *res;
if (kwargs == NULL || PyDict_GET_SIZE(kwargs) == 0) {
res = func(callable, args, nargsf, NULL);
}

@colesbury colesbury marked this pull request as ready for review February 2, 2026 21:44
@colesbury colesbury merged commit 79c43e7 into python:main Feb 3, 2026
71 of 90 checks passed
@colesbury colesbury deleted the gh-139103-stack-unpack-dict-borrowed branch February 3, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants