From 8156b8b97b5a61af965fcffb55270cf72f1c0659 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Wed, 28 Oct 2026 01:08:28 -0700 Subject: [PATCH] gh-87613: Use AC VC for io.BytesIO 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 --- Modules/_io/bytesio.c | 4 +- Modules/_io/clinic/bytesio.c.h | 73 +++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index 01f63f8a677d371..92d5eb1d8cf500b 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -1115,6 +1115,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'' @@ -1124,7 +1125,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, @@ -1253,6 +1254,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]*/