You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Generate vectorcall code to parse arguments using Argument Clinic #87613
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:
--------------
staticPyObject*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:
returnreturn_value;
}
/*[clinic input]@classmethodfloat.\_\_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]*/staticPyObject*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();
}
returnfloat_subtype_new(type, x); /* Wimp out */
}
if (x==NULL) {
returnPyFloat_FromDouble(0.0);
}
/* If it's a string, but not a string subclass, use PyFloat_FromString. */if (PyUnicode_CheckExact(x))
returnPyFloat_FromString(x);
returnPyNumber_Float(x);
}
staticPyObject*float_vectorcall(PyObject*type, PyObject*const*args,
size_tnargsf, PyObject*kwnames)
{
if (!_PyArg_NoKwnames("float", kwnames)) {
returnNULL;
}
Py_ssize_tnargs=PyVectorcall_NARGS(nargsf);
if (!_PyArg_CheckPositional("float", nargs, 0, 1)) {
returnNULL;
}
PyObject*x=nargs >= 1 ? args[0] : NULL;
returnfloat_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:
--------------
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!
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
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.
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?
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.
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.
Activity
vstinner commentedon Mar 9, 2021
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:
--------------
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:
--------------
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!
corona10 commentedon Mar 21, 2021
I agree with we should update AC to generate vectorcall.
I am going to investigate what we can :)
vstinner commentedon Nov 3, 2022
I don't have the bandwidth to work on this issue, so I just close it.
68 remaining items
encukou commentedon Sep 10, 2026
Thank you for the improvement, @cmaloney!
The docs for AC live in the devguide; would you mind making a PR there as well?
pythongh-87613: Argument Clinic vectorcall decorator (pythonGH-145381)
pythongh-87613: Argument Clinic vectorcall decorator (pythonGH-145381)
cmaloney commentedon Sep 22, 2026
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
StanFromIreland commentedon Sep 25, 2026
The
async_tree_basebenchmark onmemory.python.orgalso shows a significant regression.cmaloney commentedon Sep 25, 2026
Bisected and memory + runtime increase seems to be from gh-157213, not this change. Left more details there.
pythongh-87613: Use the AC vectorcall for bytes and bytearray
pythongh-87613: Add bytes/bytearray constructor benchmarks
Merge remote-tracking branch 'upstream/main' into pythongh-87613-float2
gh-87613: Use AC vectorcall for float (GH-158201)
cmaloney commentedon Oct 13, 2026
Continuing to iterate on this. For
str,list, andintthe 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 theiftree 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
bytesandbytearray. That turns on one of the core things I hope to enable with this work: minimal copybytes(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.vstinner commentedon Oct 13, 2026
What is missing from AC generated code to get the same speed?
cmaloney commentedon Oct 13, 2026
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
In the first iteration I called this
zero_arg,one_argetc. 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_implare 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.