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. vstinner commented on Mar 9, 2021

    @vstinner
    MemberAuthor

    To optimize the creation of objects, a lot of "tp_new" methods are defined twice: once in the legacy way (tp_new slot), once with the new VECTORCALL calling convention (tp_vectorcall slot). My concern is that the VECTORCALL implementation copy/paste most of the code just to parse arguments, whereas the specific code is just a few lines.

    Example with the float type constructor:
    --------------

    static PyObject *
    float_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
    {
        PyObject *return_value = NULL;
        PyObject *x = NULL;
    
        if ((type == &PyFloat_Type) &&
            !_PyArg_NoKeywords("float", kwargs)) {
            goto exit;
        }
        if (!_PyArg_CheckPositional("float", PyTuple_GET_SIZE(args), 0, 1)) {
            goto exit;
        }
        if (PyTuple_GET_SIZE(args) < 1) {
            goto skip_optional;
        }
        x = PyTuple_GET_ITEM(args, 0);
    skip_optional:
        return_value = float_new_impl(type, x);
    
    exit:
        return return_value;
    }
    
    /*[clinic input]
    @classmethod
    float.\_\_new__ as float_new
        x: object(c_default="NULL") = 0
        /
    
    Convert a string or number to a floating point number, if possible.
    [clinic start generated code]*/
    
    static PyObject *
    float_new_impl(PyTypeObject *type, PyObject *x)
    /*[clinic end generated code: output=ccf1e8dc460ba6ba input=f43661b7de03e9d8]*/
    {
        if (type != &PyFloat_Type) {
            if (x == NULL) {
                x = _PyLong_GetZero();
            }
            return float_subtype_new(type, x); /* Wimp out */
        }
    
        if (x == NULL) {
            return PyFloat_FromDouble(0.0);
        }
        /* If it's a string, but not a string subclass, use
           PyFloat_FromString. */
        if (PyUnicode_CheckExact(x))
            return PyFloat_FromString(x);
        return PyNumber_Float(x);
    }
    
    static PyObject *
    float_vectorcall(PyObject *type, PyObject * const*args,
                     size_t nargsf, PyObject *kwnames)
    {
        if (!_PyArg_NoKwnames("float", kwnames)) {
            return NULL;
        }
    
        Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
        if (!_PyArg_CheckPositional("float", nargs, 0, 1)) {
            return NULL;
        }
    
        PyObject *x = nargs >= 1 ? args[0] : NULL;
        return float_new_impl((PyTypeObject *)type, x);
    }

    Here the float_new() function (tp_new slot) is implemented with Argument Clinic: float_new() C code is generated from the [clinic input] DSL: good!

    My concern is that float_vectorcall() code is hand written, it's boring to write and boring to maintain.

    Would it be possible to add a new [clinic input] DSL for vectorcall? I expect something like that:
    --------------

    static PyObject *
    float_vectorcall(PyObject *type, PyObject * const*args,
                     size_t nargsf, PyObject *kwnames)
    {
        if (!_PyArg_NoKwnames("float", kwnames)) {
            return NULL;
        }
    
        Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
        if (!_PyArg_CheckPositional("float", nargs, 0, 1)) {
            return NULL;
        }
    
        PyObject *x = nargs >= 1 ? args[0] : NULL;
        return float_vectorcall_impl(type, x);
    }
    
    static PyObject *
    float_vectorcall_impl(PyObject *type, PyObject *x)
    {
        return float_new_impl((PyTypeObject *)type, x);
    }

    where float_vectorcall() C code would be generated, and float_vectorcall_impl() would be the only part written manually. float_vectorcall_impl() gets a clean API and its body is way simpler to write and to maintain!

  2. corona10 commented on Mar 21, 2021

    @corona10
    Member

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

  3. transferred this issue fromon Apr 10, 2022
  4. vstinner commented on Nov 3, 2022

    @vstinner
    MemberAuthor

    I don't have the bandwidth to work on this issue, so I just close it.

  5. added
    type-featureA feature request or enhancement
    and removed on Aug 17, 2023
  6. 68 remaining items

  7. 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?

  8. added a commit that references this issue on Sep 12, 2026
  9. added a commit that references this issue on Sep 14, 2026
  10. 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

  11. StanFromIreland commented on Sep 25, 2026

    @StanFromIreland
    Member

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

  12. 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.

  13. added 2 commits that reference this issue on Sep 26, 2026
  14. added a commit that references this issue on Oct 13, 2026
  15. 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.

  16. 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?

  17. 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.

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