https://github.com/python/cpython/commit/61fdee34eed9ea6f03190a5ddd7bb0c7ccc87be9
commit: 61fdee34eed9ea6f03190a5ddd7bb0c7ccc87be9
branch: main
author: Cody Maloney <[email protected]>
committer: cmaloney <[email protected]>
date: 2026-11-08T12:22:40-08:00
summary:
gh-87613: Use AC VC for io.BytesIO (#160855)
BytesIO is commonly constructed in the test suite and gets significantly
faster to construct. Argument error messages are unchanged.
pyperf microbenchmark (PGO+LTO, run serially):
| Benchmark | base | bytesio |
|------------------------------|:-------:|:---------------------:|
| BytesIO(bytes) | 54.2 ns | 27.0 ns: 2.01x faster |
| BytesIO(bytearray) | 76.7 ns | 38.6 ns: 1.99x faster |
| BytesIO(initial_bytes=bytes) | 56.5 ns | 34.8 ns: 1.63x faster |
| BytesIO() | 40.9 ns | 25.4 ns: 1.61x faster |
| Geometric mean | (ref) | 1.48x faster |
Benchmark hidden because not significant (2): `BytesIO.__new__`,
subclass(bytes)
Benchmark code:
```python
"""BytesIO() construction: AC @vectorcall vs tp_new + tp_init."""
import pyperf
runner = pyperf.Runner()
setup = "from io import BytesIO; b = b'abcdefgh'"
runner.timeit('BytesIO()', 'BytesIO()', setup)
runner.timeit('BytesIO(bytes)', 'BytesIO(b)', setup)
runner.timeit('BytesIO(bytearray)', 'BytesIO(x)', setup + '; x = bytearray(b)')
runner.timeit('BytesIO(initial_bytes=bytes)', 'BytesIO(initial_bytes=b)', setup)
runner.timeit('subclass(bytes)', 'S(b)', setup + '\nclass S(BytesIO): pass')
runner.timeit('BytesIO.__new__', 'new(BytesIO)',
setup + '; new = BytesIO.__new__')
```
Co-authored-by: Claude Opus 5.5 <[email protected]>
files:
M Modules/_io/bytesio.c
M Modules/_io/clinic/bytesio.c.h
diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c
index 2571da4538363f1..9bec70acebb3598 100644
--- a/Modules/_io/bytesio.c
+++ b/Modules/_io/bytesio.c
@@ -1118,6 +1118,7 @@ bytesio_new(PyTypeObject *type, PyObject *args, PyObject
*kwds)
}
/*[clinic input]
+@vectorcall
@critical_section
_io.BytesIO.__init__
initial_bytes as initvalue: object(c_default="NULL") = b''
@@ -1127,7 +1128,7 @@ Buffered I/O implementation using an in-memory bytes
buffer.
static int
_io_BytesIO___init___impl(bytesio *self, PyObject *initvalue)
-/*[clinic end generated code: output=65c0c51e24c5b621 input=3da5a74ee4c4f1ac]*/
+/*[clinic end generated code: output=65c0c51e24c5b621 input=a6dc38082204df28]*/
{
if (FT_ATOMIC_LOAD_SSIZE_RELAXED(self->exports) > 0) {
PyErr_SetString(PyExc_BufferError,
@@ -1256,6 +1257,7 @@ static PyType_Slot bytesio_slots[] = {
{Py_tp_getset, bytesio_getsetlist},
{Py_tp_init, _io_BytesIO___init__},
{Py_tp_new, bytesio_new},
+ {Py_tp_vectorcall, _io_BytesIO_vectorcall},
{0, NULL},
};
diff --git a/Modules/_io/clinic/bytesio.c.h b/Modules/_io/clinic/bytesio.c.h
index b2305e35ca81e75..e48d71397367daf 100644
--- a/Modules/_io/clinic/bytesio.c.h
+++ b/Modules/_io/clinic/bytesio.c.h
@@ -4,11 +4,11 @@ preserve
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
# include "pycore_gc.h" // PyGC_Head
-# include "pycore_runtime.h" // _Py_ID()
#endif
#include "pycore_abstract.h" // _Py_convert_optional_to_ssize_t()
#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION()
#include "pycore_modsupport.h" // _PyArg_CheckPositional()
+#include "pycore_runtime.h" // _Py_SINGLETON()
PyDoc_STRVAR(_io_BytesIO_closed__doc__,
"True if the file is closed.");
@@ -716,7 +716,8 @@ static int
_io_BytesIO___init___impl(bytesio *self, PyObject *initvalue);
static int
-_io_BytesIO___init__(PyObject *self, PyObject *args, PyObject *kwargs)
+_io_BytesIO___init___helper(PyObject *self, PyObject *const *args,
+ Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames)
{
int return_value = -1;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
@@ -748,11 +749,10 @@ _io_BytesIO___init__(PyObject *self, PyObject *args,
PyObject *kwargs)
#undef KWTUPLE
PyObject *argsbuf[1];
PyObject * const *fastargs;
- Py_ssize_t nargs = PyTuple_GET_SIZE(args);
- Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0;
+ Py_ssize_t noptargs = nargs + nkw - 0;
PyObject *initvalue = NULL;
- fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs,
kwargs, NULL, &_parser,
+ fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser,
/*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
if (!fastargs) {
goto exit;
@@ -770,6 +770,67 @@ _io_BytesIO___init__(PyObject *self, PyObject *args,
PyObject *kwargs)
return return_value;
}
+static int
+_io_BytesIO___init__(PyObject *self, PyObject *args, PyObject *kwargs)
+{
+ return _io_BytesIO___init___helper(self, _PyTuple_CAST(args)->ob_item,
+ PyTuple_GET_SIZE(args),
+ kwargs ? PyDict_GET_SIZE(kwargs) : 0,
+ kwargs, NULL);
+}
+
+static PyObject *
+_io_BytesIO_vectorcall(PyObject *type, PyObject *const *args,
+ size_t nargsf, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
+ PyObject *self;
+ int _result;
+ PyObject *initvalue = NULL;
+
+ assert(_PyType_CAST(type)->tp_init == _io_BytesIO___init__);
+ /* Make sure the type object is immutable: the generated
+ * vectorcall doesn't deal e.g. with users reassigning __init__. */
+ assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE));
+ if (kwnames != NULL || nargs > 1) {
+ self = _PyType_CAST(type)->tp_new(_PyType_CAST(type),
+ (PyObject *)&_Py_SINGLETON(tuple_empty), NULL);
+ if (self == NULL) {
+ return NULL;
+ }
+ _result = _io_BytesIO___init___helper(self, args, nargs,
+ kwnames ? PyTuple_GET_SIZE(kwnames) : 0,
+ NULL, kwnames);
+ if (_result != 0) {
+ Py_DECREF(self);
+ return NULL;
+ }
+ return self;
+ }
+ if (nargs < 1) {
+ goto skip_optional;
+ }
+ initvalue = args[0];
+skip_optional:
+ self = _PyType_CAST(type)->tp_new(_PyType_CAST(type),
+ (PyObject *)&_Py_SINGLETON(tuple_empty), NULL);
+ if (self == NULL) {
+ goto exit;
+ }
+ Py_BEGIN_CRITICAL_SECTION(self);
+ _result = _io_BytesIO___init___impl((bytesio *)self, initvalue);
+ Py_END_CRITICAL_SECTION();
+ if (_result != 0) {
+ Py_DECREF(self);
+ goto exit;
+ }
+ return_value = self;
+
+exit:
+ return return_value;
+}
+
PyDoc_STRVAR(_io_BytesIO___sizeof____doc__,
"__sizeof__($self, /)\n"
"--\n"
@@ -795,4 +856,4 @@ _io_BytesIO___sizeof__(PyObject *self, PyObject
*Py_UNUSED(ignored))
}
#define _IO_BYTESIO_CLOSED_GETSETDEF {"closed",
(getter)_io_BytesIO_closed_get, (setter)NULL, _io_BytesIO_closed__doc__},
-/*[clinic end generated code: output=a2bc96c358d6e240 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=b9d367ad8e0b44d2 input=a9049054013a1b77]*/
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]