Skip to content

Commit 8e0d8a2

Browse files
committed
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
1 parent 85c03d4 commit 8e0d8a2

6 files changed

Lines changed: 213 additions & 13 deletions

File tree

‎Lib/test/test_bytes.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import sys
1717
import tempfile
1818
import textwrap
19+
lazy import tracemalloc
1920
import threading
2021
import unittest
2122
from _codecs import _unregister_error as _codecs_unregister_error
@@ -1809,6 +1810,23 @@ def hashing_codec(name):
18091810
self.assertEqual(taken, b'Hello')
18101811
self.assertEqual(hash(taken), hash(b'Hello'))
18111812

1813+
@support.cpython_only
1814+
@support.bigmemtest(size=support._1G, memuse=1)
1815+
def test_unique_temporary_take_memory(self, size):
1816+
# Optimization: When passed a unique temporary adopt it rather than
1817+
# allocate + copy + deallocate.
1818+
def peak(func):
1819+
tracemalloc.start()
1820+
try:
1821+
func()
1822+
return tracemalloc.get_traced_memory()[1]
1823+
finally:
1824+
tracemalloc.stop()
1825+
# Copy would be 2.0, 1.5 gives space for other allocations.
1826+
self.assertLess(peak(lambda: bytearray(bytes(size))), size * 1.5)
1827+
self.assertLess(peak(lambda: bytearray(bytearray(size))), size * 1.5)
1828+
self.assertLess(peak(lambda: bytes(bytearray(size))), size * 1.5)
1829+
18121830
def test_take_bytes_reentrant_resize(self):
18131831
# gh-153570: n.__index__() can resize the bytearray, so take_bytes()
18141832
# must re-read the size afterwards. It cached the size before the
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: 30 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;
@@ -1082,6 +1083,33 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
10821083
}
10831084
}
10841085

1086+
/* optimization: Adopt bytes rather than copying. */
1087+
if (PyBytes_CheckExact(arg)
1088+
&& PyUnstable_Object_IsUniqueReferencedTemporary(arg)
1089+
&& _PyBytes_GET_CACHED_HASH((PyBytesObject *)arg) == -1)
1090+
{
1091+
/* Reinit before taking a reference: it asserts the bytes is
1092+
uniquely referenced. */
1093+
self->ob_bytes_object = arg;
1094+
bytearray_reinit_from_bytes(self, PyBytes_GET_SIZE(arg));
1095+
Py_INCREF(arg);
1096+
return 0;
1097+
}
1098+
1099+
/* optimization: Adopt bytearray rather than copying. */
1100+
if (PyByteArray_CheckExact(arg)
1101+
&& PyUnstable_Object_IsUniqueReferencedTemporary(arg))
1102+
{
1103+
PyObject *bytes = bytearray_take_bytes_impl((PyByteArrayObject *)arg,
1104+
Py_None);
1105+
if (bytes == NULL) {
1106+
return -1;
1107+
}
1108+
self->ob_bytes_object = bytes;
1109+
bytearray_reinit_from_bytes(self, PyBytes_GET_SIZE(bytes));
1110+
return 0;
1111+
}
1112+
10851113
/* Use the buffer API */
10861114
if (PyObject_CheckBuffer(arg)) {
10871115
Py_ssize_t size;
@@ -3027,6 +3055,7 @@ PyTypeObject PyByteArray_Type = {
30273055
PyType_GenericAlloc, /* tp_alloc */
30283056
bytearray_new, /* tp_new */
30293057
PyObject_Free, /* tp_free */
3058+
.tp_vectorcall = bytearray_vectorcall,
30303059
.tp_version_tag = _Py_TYPE_VERSION_BYTEARRAY,
30313060
};
30323061

‎Objects/bytesobject.c‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2918,6 +2918,7 @@ static PyObject *
29182918
bytes_subtype_new(PyTypeObject *, PyObject *);
29192919

29202920
/*[clinic input]
2921+
@vectorcall
29212922
@classmethod
29222923
bytes.__new__ as bytes_new
29232924
@@ -2930,7 +2931,7 @@ bytes.__new__ as bytes_new
29302931
static PyObject *
29312932
bytes_new_impl(PyTypeObject *type, PyObject *x, const char *encoding,
29322933
const char *errors)
2933-
/*[clinic end generated code: output=1e0c471be311a425 input=f0a966d19b7262b4]*/
2934+
/*[clinic end generated code: output=1e0c471be311a425 input=b0248d22e221a095]*/
29342935
{
29352936
PyObject *bytes;
29362937
PyObject *func;
@@ -3002,6 +3003,11 @@ bytes_new_impl(PyTypeObject *type, PyObject *x, const char *encoding,
30023003
bytes = _PyBytes_FromSizeZero(size);
30033004
}
30043005
}
3006+
/* Adopt unique temporary bytearray rather than copying. */
3007+
else if (PyByteArray_CheckExact(x)
3008+
&& PyUnstable_Object_IsUniqueReferencedTemporary(x)) {
3009+
bytes = PyObject_CallMethodNoArgs(x, &_Py_ID(take_bytes));
3010+
}
30053011
else {
30063012
bytes = PyBytes_FromObject(x);
30073013
}
@@ -3316,6 +3322,7 @@ PyTypeObject PyBytes_Type = {
33163322
bytes_type_alloc, /* tp_alloc */
33173323
bytes_new, /* tp_new */
33183324
PyObject_Free, /* tp_free */
3325+
.tp_vectorcall = bytes_vectorcall,
33193326
.tp_version_tag = _Py_TYPE_VERSION_BYTES,
33203327
._tp_iteritem = bytes_iteritem,
33213328
};

‎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)