Skip to content

Commit 6576fb4

Browse files
cmaloneyclaude
andauthored
gh-87613: Use AC vectorcall for bytes and bytearray (#158891)
* gh-87613: Use AC vectorcall for bytes and bytearray Speeds up common construction of both objects. Enables optimizing construction off uniquely referenced temporary objects. Previously the argument tuples / dictionaries required by the call protocol resulted in arguments never being unique temporaries. With vectorcall they can be. Avoid copying by adopting the existing allocation when possible. Benchmark Run on my linux dev machine: PGO+LTO, clang 23, --with-tail-call-interp (note: AI Written, but I think sufficiently representative) ```python """bytes() and bytearray() construction. temp/ passes a unique temporary (adopted, no copy); named/ binds the same source to a name first so it is always copied. """ import pyperf runner = pyperf.Runner() runner.timeit('call/bytes()', 'bytes()') runner.timeit('call/bytes(int)', 'bytes(16)') runner.timeit('call/bytes(bytes)', "bytes(b'abcdefghabcdefgh')") runner.timeit("call/bytes(str, 'ascii')", "bytes('abcdefgh', 'ascii')") runner.timeit('call/bytearray()', 'bytearray()') runner.timeit('call/bytearray(int)', 'bytearray(16)') runner.timeit('call/bytearray(bytes)', "bytearray(b'abcdefghabcdefgh')") runner.timeit("call/bytearray(str, 'ascii')", "bytearray('abcdefgh', 'ascii')") for size_name, size in (('16B', 16), ('4KiB', 4096), ('1MiB', 1 << 20)): # Slicing in the statement makes a fresh unique temporary. setup = f"b = b'x' * {size}; ba = bytearray(b)" runner.timeit(f'temp/bytes(bytearray) {size_name}', 'bytes(ba[1:])', setup) runner.timeit(f'named/bytes(bytearray) {size_name}', 'x = ba[1:]; bytes(x); del x', setup) runner.timeit(f'temp/bytearray(bytes) {size_name}', 'bytearray(b[1:])', setup) runner.timeit(f'named/bytearray(bytes) {size_name}', 'x = b[1:]; bytearray(x); del x', setup) runner.timeit(f'temp/bytearray(bytearray) {size_name}', 'bytearray(ba[1:])', setup) runner.timeit(f'named/bytearray(bytearray) {size_name}', 'x = ba[1:]; bytearray(x); del x', setup) ``` | Benchmark | main | patch | |---------------------------------|:-------:|:----------------------:| | temp/bytearray(bytearray) 1MiB | 515 us | 14.9 us: 34.53x faster | | temp/bytearray(bytes) 1MiB | 508 us | 14.8 us: 34.27x faster | | temp/bytes(bytearray) 1MiB | 507 us | 14.9 us: 34.05x faster | | temp/bytearray(bytes) 4KiB | 144 ns | 79.5 ns: 1.81x faster | | temp/bytearray(bytearray) 4KiB | 155 ns | 94.7 ns: 1.63x faster | | temp/bytearray(bytes) 16B | 56.4 ns | 36.1 ns: 1.56x faster | | temp/bytes(bytearray) 4KiB | 150 ns | 96.4 ns: 1.55x faster | | temp/bytearray(bytearray) 16B | 64.1 ns | 47.3 ns: 1.35x faster | | call/bytes() | 14.7 ns | 11.1 ns: 1.32x faster | | call/bytes(int) | 31.5 ns | 24.3 ns: 1.29x faster | | call/bytes(str, 'ascii') | 36.9 ns | 28.7 ns: 1.28x faster | | call/bytes(bytes) | 34.3 ns | 27.3 ns: 1.26x faster | | temp/bytes(bytearray) 16B | 59.4 ns | 49.5 ns: 1.20x faster | | call/bytearray(str, 'ascii') | 45.6 ns | 39.1 ns: 1.17x faster | | call/bytearray(bytes) | 44.3 ns | 38.9 ns: 1.14x faster | | call/bytearray(int) | 41.7 ns | 37.3 ns: 1.12x faster | | named/bytearray(bytes) 16B | 59.6 ns | 54.4 ns: 1.10x faster | | named/bytes(bytearray) 16B | 61.3 ns | 56.2 ns: 1.09x faster | | named/bytearray(bytearray) 16B | 65.3 ns | 60.9 ns: 1.07x faster | | named/bytes(bytearray) 4KiB | 154 ns | 148 ns: 1.04x faster | | call/bytearray() | 22.1 ns | 21.4 ns: 1.03x faster | | named/bytearray(bytearray) 4KiB | 156 ns | 151 ns: 1.03x faster | | named/bytearray(bytes) 4KiB | 146 ns | 143 ns: 1.02x faster | | Geometric mean | (ref) | 1.77x faster | Benchmark hidden because not significant (3): named/bytes(bytearray) 1MiB, named/bytearray(bytes) 1MiB, named/bytearray(bytearray) 1MiB * Remove unique temporary adoption optimizations PyUnstable_Object_IsUniqueReferencedTemporary() can't tell when a C caller still holds a borrowed reference to the argument (for example bisect.insort(lst, bytearray(b'abc')[1:], key=bytes)), so adopting the buffer empties an object still in use. Leave the optimization for a follow-up and keep this PR to the vectorcall change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 597ae8f commit 6576fb4

5 files changed

Lines changed: 163 additions & 13 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Speed up :class:`bytes` and :class:`bytearray` construction by adding
2+
:ref:`vectorcall` support.

‎Objects/bytearrayobject.c‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,7 @@ bytearray_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
977977
}
978978

979979
/*[clinic input]
980+
@vectorcall
980981
bytearray.__init__
981982
982983
source as arg: object = NULL
@@ -988,7 +989,7 @@ bytearray.__init__
988989
static int
989990
bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
990991
const char *encoding, const char *errors)
991-
/*[clinic end generated code: output=4ce1304649c2f8b3 input=1141a7122eefd7b9]*/
992+
/*[clinic end generated code: output=4ce1304649c2f8b3 input=24ddb84055f432dd]*/
992993
{
993994
Py_ssize_t count;
994995
PyObject *it;
@@ -3027,6 +3028,7 @@ PyTypeObject PyByteArray_Type = {
30273028
PyType_GenericAlloc, /* tp_alloc */
30283029
bytearray_new, /* tp_new */
30293030
PyObject_Free, /* tp_free */
3031+
.tp_vectorcall = bytearray_vectorcall,
30303032
.tp_version_tag = _Py_TYPE_VERSION_BYTEARRAY,
30313033
};
30323034

‎Objects/bytesobject.c‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2921,6 +2921,7 @@ static PyObject *
29212921
bytes_subtype_new(PyTypeObject *, PyObject *);
29222922

29232923
/*[clinic input]
2924+
@vectorcall
29242925
@classmethod
29252926
bytes.__new__ as bytes_new
29262927
@@ -2933,7 +2934,7 @@ bytes.__new__ as bytes_new
29332934
static PyObject *
29342935
bytes_new_impl(PyTypeObject *type, PyObject *x, const char *encoding,
29352936
const char *errors)
2936-
/*[clinic end generated code: output=1e0c471be311a425 input=f0a966d19b7262b4]*/
2937+
/*[clinic end generated code: output=1e0c471be311a425 input=b0248d22e221a095]*/
29372938
{
29382939
PyObject *bytes;
29392940
PyObject *func;
@@ -3319,6 +3320,7 @@ PyTypeObject PyBytes_Type = {
33193320
bytes_type_alloc, /* tp_alloc */
33203321
bytes_new, /* tp_new */
33213322
PyObject_Free, /* tp_free */
3323+
.tp_vectorcall = bytes_vectorcall,
33223324
.tp_version_tag = _Py_TYPE_VERSION_BYTES,
33233325
._tp_iteritem = bytes_iteritem,
33243326
};

‎Objects/clinic/bytearrayobject.c.h‎

Lines changed: 89 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Objects/clinic/bytesobject.c.h‎

Lines changed: 66 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)