Skip to content

Generate vectorcall code to parse arguments using Argument Clinic #87613

Open

Description

@vstinner
BPO 43447
Nosy @vstinner, @serhiy-storchaka, @corona10, @erlend-aasland, @kumaraditya303

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = 'https://github.com/corona10'
closed_at = None
created_at = <Date 2021-03-09.11:34:23.944>
labels = ['expert-C-API', '3.10']
title = 'Generate vectorcall code to parse arguments using Argument Clinic'
updated_at = <Date 2022-03-06.09:38:05.594>
user = 'https://github.com/vstinner'

bugs.python.org fields:

activity = <Date 2022-03-06.09:38:05.594>
actor = 'kumaraditya'
assignee = 'corona10'
closed = False
closed_date = None
closer = None
components = ['C API']
creation = <Date 2021-03-09.11:34:23.944>
creator = 'vstinner'
dependencies = []
files = []
hgrepos = []
issue_num = 43447
keywords = []
message_count = 2.0
messages = ['388354', '389214']
nosy_count = 5.0
nosy_names = ['vstinner', 'serhiy.storchaka', 'corona10', 'erlendaasland', 'kumaraditya']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = None
url = 'https://bugs.python.org/issue43447'
versions = ['Python 3.10']

Linked PRs

Activity

  1. erlend-aasland commented on Aug 17, 2023

    @erlend-aasland
    Contributor

    I agree with we should update AC to generate vectorcall.
    I am going to investigate what we can :)

    Dong-hee, did you ever get to this? :) If so, I'd be interested in hearing what you found out.

  2. corona10 commented on Aug 17, 2023

    @corona10
    Member

    Dong-hee, did you ever get to this? :) If so, I'd be interested in hearing what you found out.

    Sorry, I didn't make progress on this issue. One thing I considered at that moment was supporting positional arguments only cases first. Implementing keyword arguments will be quite complex and not many use cases exist we should cover. so it is not urgent.

    If you want to take a look at this issue, it will be fine.

  3. corona10 commented on Aug 17, 2023

    @corona10
    Member

    enumrate is good example when you want to handle keyword arguments. as you can read it's too complex.

    enumerate_vectorcall(PyObject *type, PyObject *const *args,
    size_t nargsf, PyObject *kwnames)
    {
    PyTypeObject *tp = _PyType_CAST(type);
    Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
    Py_ssize_t nkwargs = 0;
    if (kwnames != NULL) {
    nkwargs = PyTuple_GET_SIZE(kwnames);
    }
    // Manually implement enumerate(iterable, start=...)
    if (nargs + nkwargs == 2) {
    if (nkwargs == 1) {
    if (!check_keyword(kwnames, 0, "start")) {
    return NULL;
    }
    } else if (nkwargs == 2) {
    PyObject *kw0 = PyTuple_GET_ITEM(kwnames, 0);
    if (_PyUnicode_EqualToASCIIString(kw0, "start")) {
    if (!check_keyword(kwnames, 1, "iterable")) {
    return NULL;
    }
    return enum_new_impl(tp, args[1], args[0]);
    }
    if (!check_keyword(kwnames, 0, "iterable") ||
    !check_keyword(kwnames, 1, "start")) {
    return NULL;
    }
    }
    return enum_new_impl(tp, args[0], args[1]);
    }
    if (nargs + nkwargs == 1) {
    if (nkwargs == 1 && !check_keyword(kwnames, 0, "iterable")) {
    return NULL;
    }
    return enum_new_impl(tp, args[0], NULL);
    }
    if (nargs == 0) {
    PyErr_SetString(PyExc_TypeError,
    "enumerate() missing required argument 'iterable'");
    return NULL;
    }
    PyErr_Format(PyExc_TypeError,
    "enumerate() takes at most 2 arguments (%d given)", nargs + nkwargs);
    return NULL;
    }

  4. erlend-aasland commented on Aug 17, 2023

    @erlend-aasland
    Contributor

    Sorry, I didn't make progress on this issue. One thing I considered at that moment was supporting positional arguments only cases first. Implementing keyword arguments will be quite complex and not many use cases exist we should cover. so it is not urgent.

    If you want to take a look at this issue, it will be fine.

    No need to be sorry :) I don't have the bandwidth to look at it right now, but I hope to be able to pick it up this fall. Let's keep the issue open. If you find time for this, that is great, but there is no urgency :)

  5. cmaloney commented on Nov 9, 2025

    @cmaloney
    Contributor

    Taking a look at this scoped down to __new__ and __init__ which should be able to do float, bytes, str, and bytearray to simplify and optimize some cases. My plan is to try making a @vectorcall decorator in clinic which causes two parse arg codegens to output: one for the existing, a second which adds an _vectorcall variant. What I'm experimenting with is the following input:

    /*[clinic input]
    @vectorcall
    Test.__init__
        a: object
        /
        ab: object = None
    Vectorcall __init__ with args and kwargs.
    [clinic start generated code]*/
    
  6. kumaraditya303 commented on Dec 15, 2025

    @kumaraditya303
    Contributor

    My plan is to try making a @vectorcall decorator in clinic which causes two parse arg codegens to output: one for the existing, a second which adds an _vectorcall variant.

    That sounds great, I am looking forward to use it to speed up asyncio task creation which would benefit from this.

  7. added 20 commits that reference this issue on Feb 22, 2026
  8. added 20 commits that reference this issue on Feb 25, 2026
  9. added 18 commits that reference this issue on Feb 25, 2026
  10. cmaloney commented on Mar 1, 2026

    @cmaloney
    Contributor

    Have a working implementation with performance matching or beating the currently handwritten implementations. Benchmark and full numbers below. Overall 1.03x faster.

    First PR: GH-145381. The changes I have split across a couple commits in my draft ac_vectorcall_v1 branch. Did a PR for just adding the support to AC; then not sure what would be best for the other changes (leaning: PR per type for easy reverts if needed?).

    • Adding @vectorcall decorator and codegen with test
    • Commit per type, replacing hand-coded vectorcall implementations with AC generated ones
    • Adding @vectorcall to additional types.
    • Microbenchmark for different constructions. Not planning to try and get into CPython; think it could be useful to track perf over time but not sure a good place for it to live.

    Performance numbers

    Benchmark code: cmaloney@ddcd3b6
    Configure options: ./configure --enable-optimizations --with-lto --with-static-libpython
    Host: 64 bit Arch Linux

    Benchmark bench_baseline2 bench_vectorcall2
    list(tuple) 303 ns 291 ns: 1.04x faster
    list_subclass 385 ns 395 ns: 1.02x slower
    bytes() 111 ns 89.5 ns: 1.24x faster
    bytes(int) 281 ns 224 ns: 1.26x faster
    bytearray() 210 ns 193 ns: 1.09x faster
    bytearray(bytes) 445 ns 394 ns: 1.13x faster
    bytearray(int) 190 ns 178 ns: 1.06x faster
    int() 73.5 ns 71.4 ns: 1.03x faster
    int(str) 175 ns 182 ns: 1.04x slower
    enumerate(list) 318 ns 329 ns: 1.04x slower
    Geometric mean (ref) 1.03x faster

    Benchmark hidden because not significant (13): list(), list(range), float(), float(int), float(str), str(), str(int), str(bytes,enc), tuple(), tuple(list), int(str,base), reversed(list), enumerate(list,start)

  11. added a commit that references this issue on Sep 10, 2026
  12. encukou commented on Sep 10, 2026

    @encukou
    Member

    Thank you for the improvement, @cmaloney!

    The docs for AC live in the devguide; would you mind making a PR there as well?

  13. added a commit that references this issue on Sep 12, 2026
  14. added a commit that references this issue on Sep 14, 2026
  15. cmaloney commented on Sep 22, 2026

    @cmaloney
    Contributor

    Working on the devguide updates + moving more types w/ pyperformance numbers. A number of benchmarks seemed to get slower on the day this was merged / September 10th (ex. https://speed.python.org/timeline/#/?exe=12&ben=async_tree_eager&env=1&revs=50&equid=off&quarts=on&extr=on). Investigating the root cause.

    Cleaned this up to no longer require static C type declarations and updated existing clinic tests. Adjusts the codegen type checks slightly but I think overall nicer: #157968

  16. StanFromIreland commented on Sep 25, 2026

    @StanFromIreland
    Member

    The async_tree_base benchmark on memory.python.org also shows a significant regression.

  17. cmaloney commented on Sep 25, 2026

    @cmaloney
    Contributor

    Bisected and memory + runtime increase seems to be from gh-157213, not this change. Left more details there.

  18. added 2 commits that reference this issue on Sep 26, 2026
  19. added a commit that references this issue on Oct 13, 2026
  20. cmaloney commented on Oct 13, 2026

    @cmaloney
    Contributor

    Continuing to iterate on this. For str, list, and int the generated vectorcall is slower than the hand-written vectorcall because the hand written inlines special cases which Argument Clinic has no way, currently, of knowing about. The slowdown is ~20% on microbenchmarks. I built a prototype I call "pyspec" which would make these inlnining critical special cases visible to AC as well as available to pieces like the JIT. The core is to use plain python to write the if tree to get to C functions then lower that to C. That makes it so AC no longer reuqires the custom language/parser. Nothing revolutionary in the python ecosystems but I think has some nice side-effects.

    This currently only supports static types. GH-157968 makes it support non-static types. That is necessary to support Decimal. That makes both heap and static types supported and tested. It would be nice to get reviewed as it changes the asserts in generated code a little.

    As a demonstration of speedup and applying to new types I put together GH-158891 for bytes and bytearray. That turns on one of the core things I hope to enable with this work: minimal copy bytes(bytearray(bytes(bytearray(b'123'))). Before Vectorcall there were references in arguments so every constructor had to copy. Now there is a uniquely referenced temporaty where we can "pass along" the storage avoiding copies.

  21. vstinner commented on Oct 13, 2026

    @vstinner
    MemberAuthor

    Continuing to iterate on this. For str, list, and int the generated vectorcall is slower than the hand-written vectorcall because the hand written inlines special cases which Argument Clinic has no way, currently, of knowing about.

    What is missing from AC generated code to get the same speed?

  22. cmaloney commented on Oct 13, 2026

    @cmaloney
    Contributor

    AC needs to be able to direct dispatch / inline the "special" cases of 0, 1, and 2:

    cpython/Objects/longobject.c

    Lines 6529 to 6549 in 64bad23

    long_vectorcall(PyObject *type, PyObject * const*args,
    size_t nargsf, PyObject *kwnames)
    {
    Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
    if (kwnames != NULL) {
    PyThreadState *tstate = PyThreadState_GET();
    return _PyObject_MakeTpCall(tstate, type, args, nargs, kwnames);
    }
    switch (nargs) {
    case 0:
    return _PyLong_GetZero();
    case 1:
    return PyNumber_Long(args[0]);
    case 2:
    return long_new_impl(_PyType_CAST(type), args[0], args[1]);
    default:
    return PyErr_Format(PyExc_TypeError,
    "int expected at most 2 arguments, got %zd",
    nargs);
    }
    }

    In the first iteration I called this zero_arg, one_arg etc. To solve that I'm hoping to make the information available both to Argument Clinic and projects like the JIT. That was too specialized of infra in a lot of ways.

    Currently functions like bytes_new_impl are fully opaque. I'm hoping to make them be less opaque by giving the base decision tree in some Python syntax. Need some help from core devs what is preferred (an if tree like existing code, match statements, overloads, ...). That keeps to very basic operations and can lower directly from AST -> C as well as building an "overload" table that the JIT can potentially consume.

    My prototype of a simple stub: https://github.com/cmaloney/cpython/blob/7abb5c4b7d1d9925e837d14205c6658ef97ae051/Objects/pyspec/bytesobject.py#L139-L155

    And the full bytes __new__: https://github.com/cmaloney/cpython/blob/7abb5c4b7d1d9925e837d14205c6658ef97ae051/Objects/pyspec/bytesobject.py#L69-L122

    The hope is we can have one core definition which we can efficiently codegen to the set of functions we need + metadata that says "single unique bytearray arg -> use this C function directly". That should reduce the number of instructions needed while keeping things maintainable. Argument for pushing towards Python syntax is to make it look just like everything else the JIT must optimize / less special cases and less custom parsers.

  23. added a commit that references this issue on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions