Skip to content

Commit 6d31270

Browse files
cmaloneyclaudeStanFromIreland
authored
gh-87613: AC VC add support for *args and use it for zip (#160944)
`@vectorcall` accepted `*args`, but the generated code expected an args tuple. Use fastcall form to parse the args array. zip had no vectorcall, so every call built an args tuple and a kwargs dict, and strict=True also went through PyArg_ParseTupleAndKeywords with a temporary empty tuple. Argument error messages are unchanged except for unknown keywords: `zip(strict=1, x=2)` now reports "got an unexpected keyword argument 'x'" instead of "takes at most 1 keyword argument (2 given)". The docstring gains a summary line; `__text_signature__` is unchanged. pyperf microbenchmark (PGO+LTO, run serially): | Benchmark | base | zip | |------------------------|:-------:|:---------------------:| | zip(a, b, strict=True) | 118 ns | 61.5 ns: 1.92x faster | | zip(a, b) | 64.9 ns | 53.1 ns: 1.22x faster | | zip(a) | 55.0 ns | 47.1 ns: 1.17x faster | | list(zip(a, b)) | 123 ns | 113 ns: 1.09x faster | | zip() | 27.4 ns | 25.7 ns: 1.07x faster | | Sub(a, b) | 71.5 ns | 70.6 ns: 1.01x faster | Benchmark hidden because not significant (1): zip.`__new__` Benchmark code: ```python import pyperf runner = pyperf.Runner() setup = "a = (1, 2, 3); b = [4, 5, 6]" for stmt in ["zip()", "zip(a)", "zip(a, b)", "zip(a, b, strict=True)", "list(zip(a, b))"]: runner.timeit(stmt, stmt, setup) runner.timeit('zip.`__new__`', 'new(zip, a, b)', setup + '; new = zip.__new__') runner.timeit('Sub(a, b)', 'S(a, b)', setup + '\nclass S(zip): pass') ``` Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Stan Ulbrych <stan@python.org>
1 parent ce841a9 commit 6d31270

4 files changed

Lines changed: 188 additions & 54 deletions

File tree

‎Lib/test/clinic.test.c‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5220,6 +5220,7 @@ Test___init___impl(TestObj *self, PyObject *args)
52205220
/*[clinic end generated code: output=0e5836c40dbc2397 input=a615a4485c0fc3e2]*/
52215221

52225222
/*[clinic input]
5223+
@vectorcall
52235224
@classmethod
52245225
Test.__new__
52255226
*args: tuple
@@ -5261,9 +5262,37 @@ Test(PyTypeObject *type, PyObject *args, PyObject *kwargs)
52615262
return return_value;
52625263
}
52635264

5265+
static PyObject *
5266+
Test_vectorcall(PyObject *type, PyObject *const *args,
5267+
size_t nargsf, PyObject *kwnames)
5268+
{
5269+
PyObject *return_value = NULL;
5270+
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
5271+
PyObject *__clinic_args = NULL;
5272+
5273+
assert(_PyType_CAST(type)->tp_new == Test);
5274+
/* Make sure the type object is immutable: the generated
5275+
* vectorcall doesn't deal e.g. with users reassigning __init__. */
5276+
assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE));
5277+
if (!_PyArg_NoKwnames("Test", kwnames)) {
5278+
goto exit;
5279+
}
5280+
__clinic_args = PyTuple_FromArray(args, nargs);
5281+
if (__clinic_args == NULL) {
5282+
goto exit;
5283+
}
5284+
return_value = Test_impl(_PyType_CAST(type), __clinic_args);
5285+
5286+
exit:
5287+
/* Cleanup for args */
5288+
Py_XDECREF(__clinic_args);
5289+
5290+
return return_value;
5291+
}
5292+
52645293
static PyObject *
52655294
Test_impl(PyTypeObject *type, PyObject *args)
5266-
/*[clinic end generated code: output=e6fba0c8951882fd input=8ce30adb836aeacb]*/
5295+
/*[clinic end generated code: output=07f68e3971ed24cc input=1dc824fd6cec5f8e]*/
52675296

52685297

52695298
/*[clinic input]

‎Python/bltinmodule.c‎

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3139,41 +3139,45 @@ typedef struct {
31393139

31403140
#define _zipobject_CAST(op) ((zipobject *)(op))
31413141

3142+
/*[clinic input]
3143+
class zip "zipobject *" "&PyZip_Type"
3144+
@vectorcall
3145+
@classmethod
3146+
zip.__new__ as zip_new
3147+
3148+
*iterables: array
3149+
strict: bool = False
3150+
3151+
Iterate over several iterables in parallel, yielding tuples.
3152+
3153+
The zip object yields n-length tuples, where n is the number of
3154+
iterables passed as positional arguments to zip(). The i-th element
3155+
in every tuple comes from the i-th iterable argument to zip(). This
3156+
continues until the shortest argument is exhausted.
3157+
3158+
If strict is true and one of the arguments is exhausted before the
3159+
others, raise a ValueError.
3160+
3161+
>>> list(zip('abcdefg', range(3), range(4)))
3162+
[('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]
3163+
[clinic start generated code]*/
3164+
31423165
static PyObject *
3143-
zip_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
3166+
zip_new_impl(PyTypeObject *type, PyObject * const *iterables,
3167+
Py_ssize_t iterables_length, int strict)
3168+
/*[clinic end generated code: output=5e1a30829ce1f6ab input=4bd8e6a24e4bfb2e]*/
31443169
{
31453170
zipobject *lz;
31463171
Py_ssize_t i;
31473172
PyObject *ittuple; /* tuple of iterators */
31483173
PyObject *result;
3149-
Py_ssize_t tuplesize;
3150-
int strict = 0;
3151-
3152-
if (kwds) {
3153-
PyObject *empty = PyTuple_New(0);
3154-
if (empty == NULL) {
3155-
return NULL;
3156-
}
3157-
static char *kwlist[] = {"strict", NULL};
3158-
int parsed = PyArg_ParseTupleAndKeywords(
3159-
empty, kwds, "|$p:zip", kwlist, &strict);
3160-
Py_DECREF(empty);
3161-
if (!parsed) {
3162-
return NULL;
3163-
}
3164-
}
3165-
3166-
/* args must be a tuple */
3167-
assert(PyTuple_Check(args));
3168-
tuplesize = PyTuple_GET_SIZE(args);
31693174

31703175
/* obtain iterators */
3171-
ittuple = PyTuple_New(tuplesize);
3176+
ittuple = PyTuple_New(iterables_length);
31723177
if (ittuple == NULL)
31733178
return NULL;
3174-
for (i=0; i < tuplesize; ++i) {
3175-
PyObject *item = PyTuple_GET_ITEM(args, i);
3176-
PyObject *it = PyObject_GetIter(item);
3179+
for (i=0; i < iterables_length; ++i) {
3180+
PyObject *it = PyObject_GetIter(iterables[i]);
31773181
if (it == NULL) {
31783182
Py_DECREF(ittuple);
31793183
return NULL;
@@ -3182,12 +3186,12 @@ zip_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
31823186
}
31833187

31843188
/* create a result holder */
3185-
result = PyTuple_New(tuplesize);
3189+
result = PyTuple_New(iterables_length);
31863190
if (result == NULL) {
31873191
Py_DECREF(ittuple);
31883192
return NULL;
31893193
}
3190-
for (i=0 ; i < tuplesize ; i++) {
3194+
for (i=0 ; i < iterables_length ; i++) {
31913195
PyTuple_SET_ITEM(result, i, Py_NewRef(Py_None));
31923196
}
31933197

@@ -3199,7 +3203,7 @@ zip_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
31993203
return NULL;
32003204
}
32013205
lz->ittuple = ittuple;
3202-
lz->tuplesize = tuplesize;
3206+
lz->tuplesize = iterables_length;
32033207
lz->result = result;
32043208
lz->strict = strict;
32053209

@@ -3345,21 +3349,6 @@ static PyMethodDef zip_methods[] = {
33453349
{NULL} /* sentinel */
33463350
};
33473351

3348-
PyDoc_STRVAR(zip_doc,
3349-
"zip(*iterables, strict=False)\n\
3350-
--\n\
3351-
\n\
3352-
The zip object yields n-length tuples, where n is the number of\n\
3353-
iterables passed as positional arguments to zip(). The i-th element\n\
3354-
in every tuple comes from the i-th iterable argument to zip(). This\n\
3355-
continues until the shortest argument is exhausted.\n\
3356-
\n\
3357-
If strict is true and one of the arguments is exhausted before the\n\
3358-
others, raise a ValueError.\n\
3359-
\n\
3360-
>>> list(zip('abcdefg', range(3), range(4)))\n\
3361-
[('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]");
3362-
33633352
PyTypeObject PyZip_Type = {
33643353
PyVarObject_HEAD_INIT(&PyType_Type, 0)
33653354
"zip", /* tp_name */
@@ -3383,7 +3372,7 @@ PyTypeObject PyZip_Type = {
33833372
0, /* tp_as_buffer */
33843373
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
33853374
Py_TPFLAGS_BASETYPE, /* tp_flags */
3386-
zip_doc, /* tp_doc */
3375+
zip_new__doc__, /* tp_doc */
33873376
zip_traverse, /* tp_traverse */
33883377
0, /* tp_clear */
33893378
0, /* tp_richcompare */
@@ -3402,6 +3391,7 @@ PyTypeObject PyZip_Type = {
34023391
PyType_GenericAlloc, /* tp_alloc */
34033392
zip_new, /* tp_new */
34043393
PyObject_GC_Del, /* tp_free */
3394+
.tp_vectorcall = zip_vectorcall,
34053395
};
34063396

34073397

‎Python/clinic/bltinmodule.c.h‎

Lines changed: 106 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Tools/clinic/libclinic/parse_args.py‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -923,14 +923,14 @@ def render_option_groups_parse_tuple(
923923
parser_code.append(" }}")
924924
return parser_code
925925

926-
def _parse_vararg(self) -> str:
926+
def _parse_vararg(self, *, fastcall: bool) -> str:
927927
assert self.varpos is not None
928928
c = self.varpos.converter
929929
assert isinstance(c, libclinic.converters.VarPosCConverter)
930930
return c.parse_vararg(pos_only=self.pos_only,
931931
min_pos=self.min_pos,
932932
max_pos=self.max_pos,
933-
fastcall=self.fastcall,
933+
fastcall=fastcall,
934934
limited_capi=self.limited_capi)
935935

936936
def _parse_kwarg(self) -> str:
@@ -963,6 +963,7 @@ def _parse_positional_args(
963963
argname_fmt: str,
964964
nargs: str,
965965
limited_capi: bool,
966+
fastcall: bool,
966967
) -> list[str] | None:
967968
"""Emit per-parameter positional argument parsing.
968969
@@ -995,8 +996,8 @@ def _parse_positional_args(
995996
if self.min_pos < len(self.parameters):
996997
parser_code.append("skip_optional:")
997998
if self.varpos:
998-
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(),
999-
indent=4))
999+
parser_code.append(libclinic.normalize_snippet(
1000+
self._parse_vararg(fastcall=fastcall), indent=4))
10001001
elif self.var_keyword:
10011002
parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(),
10021003
indent=4))
@@ -1090,7 +1091,7 @@ def parse_pos_only(self) -> None:
10901091

10911092
pos_code = self._parse_positional_args(
10921093
argname_fmt=argname_fmt, nargs=nargs,
1093-
limited_capi=self.limited_capi)
1094+
limited_capi=self.limited_capi, fastcall=self.fastcall)
10941095
if pos_code is not None:
10951096
parser_code.extend(pos_code)
10961097
else:
@@ -1148,7 +1149,8 @@ def parse_var_keyword(self) -> None:
11481149
parser_code.append("skip_optional:")
11491150

11501151
if self.varpos:
1151-
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
1152+
parser_code.append(libclinic.normalize_snippet(
1153+
self._parse_vararg(fastcall=self.fastcall), indent=4))
11521154
if self.var_keyword:
11531155
parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4))
11541156
self.parser_body(*parser_code)
@@ -1376,7 +1378,12 @@ def parse_general(self, clang: CLanguage) -> None:
13761378
if add_label:
13771379
parser_code.append("%s:" % add_label)
13781380
if self.varpos:
1379-
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
1381+
# The vectorcall helper gets an array of arguments, not a
1382+
# tuple: use the converters' fastcall convention, which
1383+
# handles that.
1384+
fastcall = True if self.func.vectorcall else self.fastcall
1385+
parser_code.append(libclinic.normalize_snippet(
1386+
self._parse_vararg(fastcall=fastcall), indent=4))
13801387
else:
13811388
self.use_converters()
13821389

@@ -1617,8 +1624,11 @@ def _vectorcall_positional(self, *,
16171624
16181625
arity_checked: Already have a number of arguments check.
16191626
"""
1627+
# The vectorcall gets an array of arguments, not a tuple: use the
1628+
# converters' fastcall convention, which handles that.
16201629
pos_code = self._parse_positional_args(
1621-
argname_fmt='args[%d]', nargs='nargs', limited_capi=False)
1630+
argname_fmt='args[%d]', nargs='nargs', limited_capi=False,
1631+
fastcall=True)
16221632
# Converter support was validated when @vectorcall was parsed.
16231633
assert pos_code is not None
16241634
if arity_checked:

0 commit comments

Comments
 (0)