https://github.com/python/cpython/commit/8dba03970833d67b5b97dc98e1be8423f9f1d908
commit: 8dba03970833d67b5b97dc98e1be8423f9f1d908
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-10-04T21:01:31Z
summary:
gh-158585: Optimize bytes object creation (#158664)
Add bytes_alloc() helper function. bytes_alloc() and
_PyBytes_FromStringAndSize() have less conditional branches than
previous code, and so should be a little bit faster.
* Rename existing bytes_alloc() to bytes_type_alloc().
* Replace _PyBytes_FromSize(calloc=1) with _PyBytes_FromSizeZero().
* Add set_ob_shash_unsafe(): similar to set_ob_shash() but don't use
an atomic operation on Free Threading. Use this new function
on newly allocated bytes objects and in bytes_resize_inplace().
* Replace PyBytes_FromStringAndSize(NULL, size) with
bytes_alloc(size).
* Use bytes_alloc() in PyBytes_FromString() and _PyBytes_Repeat().
* Add _PyBytes_FromStringAndSize(): similar to
PyBytes_FromStringAndSize(), but str must not be NULL.
* Replace PyBytes_FromStringAndSize() with
_PyBytes_FromStringAndSize().
* Test that PyBytes_FromString() and PyBytes_FromStringAndSize()
return singletons for 0 or 1 bytes.
* Fix test_tracemalloc.test_set_traceback_limit().
files:
M Lib/test/test_bytes.py
M Lib/test/test_capi/test_bytes.py
M Lib/test/test_tracemalloc.py
M Objects/bytesobject.c
diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py
index 0f2eec38631213..f87a924c57016e 100644
--- a/Lib/test/test_bytes.py
+++ b/Lib/test/test_bytes.py
@@ -410,6 +410,8 @@ def test_concat(self):
self.assertRaises(TypeError, lambda: b1 + "def")
self.assertRaises(TypeError, lambda: "abc" + b2)
+ self.assertEqual(self.type2test() + memoryview(b''), b'')
+
def test_repeat(self):
for b in b"abc", self.type2test(b"abc"):
self.assertEqual(b * 3, b"abcabcabc")
@@ -1447,6 +1449,45 @@ class SubBytes(bytes):
self.assertNotEqual(id(s), id(1 * s))
self.assertNotEqual(id(s), id(s * 2))
+ @support.cpython_only
+ def test_concat_cpython(self):
+ # Test optimizations
+ empty = b''
+ abc = b"abc"
+ self.assertIs(abc + empty, abc)
+ self.assertIs(empty + abc, abc)
+ self.assertIs(empty + bytearray(b''), empty)
+ self.assertIs(empty + memoryview(b''), empty)
+
+ class Subclass(bytes):
+ pass
+
+ empty_subclass = Subclass(b"")
+ self.assertIs(empty + empty_subclass, empty)
+ self.assertIs(empty_subclass + empty, empty)
+ self.assertIs(abc + empty_subclass, abc)
+ self.assertIs(empty_subclass + abc, abc)
+
+ def assert_copy(result, expected):
+ self.assertIsNot(result, expected)
+ self.assertEqual(type(result), bytes)
+ self.assertEqual(result, expected)
+
+ # Copy the string if it's a subclass
+ def_subclass = Subclass(b"def")
+ assert_copy(def_subclass + empty, b'def')
+ assert_copy(empty + def_subclass, b'def')
+ assert_copy(abc + def_subclass, b'abcdef')
+ assert_copy(def_subclass + abc, b'defabc')
+
+ @support.cpython_only
+ def test_repeat_cpython(self):
+ # Test optimizations
+ for b in b'', b'x', b"abc":
+ self.assertIs(b * 1, b)
+ self.assertIs(b * 0, b'')
+ self.assertIs(b * -23, b'')
+
class ByteArrayTest(BaseBytesTest, unittest.TestCase):
type2test = bytearray
diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py
index ea5d3a3d59b6fd..0f53d915c63dd3 100644
--- a/Lib/test/test_capi/test_bytes.py
+++ b/Lib/test/test_capi/test_bytes.py
@@ -61,10 +61,12 @@ def test_fromstringandsize(self):
self.assertEqual(fromstringandsize(b'abc'), b'abc')
self.assertEqual(fromstringandsize(b'abc', 2), b'ab')
self.assertEqual(fromstringandsize(b'abc\0def'), b'abc\0def')
- self.assertEqual(fromstringandsize(b'a'), b'a')
- self.assertEqual(fromstringandsize(b'a', 1), b'a')
- self.assertEqual(fromstringandsize(b'', 0), b'')
- self.assertEqual(fromstringandsize(NULL, 0), b'')
+
+ # Return singleton for 0 or 1 bytes
+ self.assertIs(fromstringandsize(b'', 0), b'')
+ self.assertIs(fromstringandsize(NULL, 0), b'')
+ self.assertIs(fromstringandsize(b'a', 1), b'a')
+
self.assertEqual(len(fromstringandsize(NULL, 3)), 3)
self.assertRaises((MemoryError, OverflowError),
fromstringandsize, NULL, PY_SSIZE_T_MAX)
@@ -79,7 +81,10 @@ def test_fromstring(self):
fromstring = _testlimitedcapi.bytes_fromstring
self.assertEqual(fromstring(b'abc\0def'), b'abc')
- self.assertEqual(fromstring(b''), b'')
+
+ # Return singleton for 0 or 1 bytes
+ self.assertIs(fromstring(b''), b'')
+ self.assertIs(fromstring(b'a'), b'a')
# CRASHES fromstring(NULL)
diff --git a/Lib/test/test_tracemalloc.py b/Lib/test/test_tracemalloc.py
index ee02fbd16af0c4..32a869c706270d 100644
--- a/Lib/test/test_tracemalloc.py
+++ b/Lib/test/test_tracemalloc.py
@@ -41,6 +41,8 @@ def get_frames(nframe, lineno_delta):
def allocate_bytes(size):
nframe = tracemalloc.get_traceback_limit()
bytes_len = (size - EMPTY_STRING_SIZE)
+ if bytes_len < 0:
+ raise ValueError(f"{size} bytes is too small")
frames = get_frames(nframe, 1)
data = b'x' * bytes_len
return data, tracemalloc.Traceback(frames, min(len(frames), nframe))
@@ -158,7 +160,7 @@ def test_new_reference(self):
self.assertEqual(traceback, obj_traceback)
def test_set_traceback_limit(self):
- obj_size = 10
+ obj_size = 123
tracemalloc.stop()
self.assertRaises(ValueError, tracemalloc.start, -1)
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index e7bc465a87f4a0..af5dce763e1f25 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -74,6 +74,21 @@ _Py_COMP_DIAG_IGNORE_DEPR_DECLS
_Py_COMP_DIAG_POP
}
+// Similar to set_ob_shash() but don't use an atomic operation on Free
+// Threading. This function must only be used on newly allocated bytes objects.
+static inline void
+set_ob_shash_unsafe(PyBytesObject *a, Py_hash_t hash)
+{
+ // Don't test _PyObject_IsUniquelyReferenced(), so the function can be
+ // called by bytes_resize_inplace().
+ assert(Py_REFCNT(a) == 1);
+
+_Py_COMP_DIAG_PUSH
+_Py_COMP_DIAG_IGNORE_DEPR_DECLS
+ a->ob_shash = hash;
+_Py_COMP_DIAG_POP
+}
+
#define get_ob_shash(op) _PyBytes_GET_CACHED_HASH(op)
@@ -99,8 +114,36 @@ _Py_COMP_DIAG_POP
PyBytes_FromStringAndSize()) or the length of the string in the 'str'
parameter (for PyBytes_FromString()).
*/
+
+// Allocate a new bytes object with contents left uninitialized. The caller is
+// responsible to fill the contents. size must be greater than 0.
static PyObject *
-_PyBytes_FromSize(Py_ssize_t size, int use_calloc)
+bytes_alloc(Py_ssize_t size)
+{
+ assert(size >= 1);
+
+ if ((size_t)size > (size_t)PY_SSIZE_T_MAX - PyBytesObject_SIZE) {
+ PyErr_SetString(PyExc_OverflowError,
+ "byte string is too large");
+ return NULL;
+ }
+ size_t alloc = PyBytesObject_SIZE + size;
+
+ /* Inline PyObject_NewVar */
+ PyBytesObject *op = (PyBytesObject *)PyObject_Malloc(alloc);
+ if (op == NULL) {
+ return PyErr_NoMemory();
+ }
+
+ _PyObject_InitVar(_PyVarObject_CAST(op), &PyBytes_Type, size);
+ set_ob_shash_unsafe(op, -1);
+ op->ob_sval[size] = '\0';
+ return (PyObject *) op;
+}
+
+// Create a new bytes object with contents initialized to zero. size can be 0.
+static PyObject *
+_PyBytes_FromSizeZero(Py_ssize_t size)
{
PyBytesObject *op;
assert(size >= 0);
@@ -114,77 +157,77 @@ _PyBytes_FromSize(Py_ssize_t size, int use_calloc)
"byte string is too large");
return NULL;
}
+ size_t alloc = PyBytesObject_SIZE + size;
/* Inline PyObject_NewVar */
- if (use_calloc)
- op = (PyBytesObject *)PyObject_Calloc(1, PyBytesObject_SIZE + size);
- else
- op = (PyBytesObject *)PyObject_Malloc(PyBytesObject_SIZE + size);
+ op = (PyBytesObject *)PyObject_Calloc(1, alloc);
if (op == NULL) {
return PyErr_NoMemory();
}
_PyObject_InitVar((PyVarObject*)op, &PyBytes_Type, size);
- set_ob_shash(op, -1);
- if (!use_calloc) {
- op->ob_sval[size] = '\0';
+ set_ob_shash_unsafe(op, -1);
+ // op->ob_sval[size] is equal to '\0'
+ return (PyObject *) op;
+}
+
+// Similar to PyBytes_FromStringAndSize(), but str must not be NULL.
+static PyObject *
+_PyBytes_FromStringAndSize(const char *str, Py_ssize_t size)
+{
+ assert(str != NULL);
+ assert(size >= 0);
+
+ if (size == 0) {
+ return bytes_get_empty();
}
+ if (size == 1) {
+ return bytes_get_char((uint8_t)str[0]);
+ }
+
+ PyBytesObject *op = (PyBytesObject *)bytes_alloc(size);
+ if (op == NULL) {
+ return NULL;
+ }
+ memcpy(op->ob_sval, str, size);
return (PyObject *) op;
}
PyObject *
PyBytes_FromStringAndSize(const char *str, Py_ssize_t size)
{
- PyBytesObject *op;
if (size < 0) {
PyErr_SetString(PyExc_SystemError,
"Negative size passed to PyBytes_FromStringAndSize");
return NULL;
}
- if (size == 1 && str != NULL) {
- return bytes_get_char((uint8_t)*str);
+
+ if (str != NULL) {
+ return _PyBytes_FromStringAndSize(str, size);
}
- if (size == 0) {
- return bytes_get_empty();
+ else {
+ if (size == 0) {
+ return bytes_get_empty();
+ }
+ return bytes_alloc(size);
}
-
- op = (PyBytesObject *)_PyBytes_FromSize(size, 0);
- if (op == NULL)
- return NULL;
- if (str == NULL)
- return (PyObject *) op;
-
- memcpy(op->ob_sval, str, size);
- return (PyObject *) op;
}
PyObject *
PyBytes_FromString(const char *str)
{
- size_t size;
- PyBytesObject *op;
-
assert(str != NULL);
- size = strlen(str);
- if (size > PY_SSIZE_T_MAX - PyBytesObject_SIZE) {
- PyErr_SetString(PyExc_OverflowError,
- "byte string is too long");
- return NULL;
- }
-
+ size_t size = strlen(str);
if (size == 0) {
return bytes_get_empty();
}
- else if (size == 1) {
- return bytes_get_char((uint8_t)*str);
+ if (size == 1) {
+ return bytes_get_char((uint8_t)str[0]);
}
- /* Inline PyObject_NewVar */
- op = (PyBytesObject *)PyObject_Malloc(PyBytesObject_SIZE + size);
+ PyBytesObject *op = (PyBytesObject *)bytes_alloc(size);
if (op == NULL) {
return PyErr_NoMemory();
}
- _PyObject_InitVar((PyVarObject*)op, &PyBytes_Type, size);
- set_ob_shash(op, -1);
memcpy(op->ob_sval, str, size+1);
return (PyObject *) op;
}
@@ -489,7 +532,7 @@ formatfloat(PyObject *v, Py_ssize_t argidx, PyObject *key,
return str;
}
- result = PyBytes_FromStringAndSize(p, len);
+ result = _PyBytes_FromStringAndSize(p, len);
PyMem_Free(p);
*p_result = result;
return result != NULL ? str : NULL;
@@ -743,8 +786,7 @@ _PyBytes_FormatEx(const char *format, Py_ssize_t format_len,
(Py_ssize_t)(fmtstart - format - 1));
goto error;
}
- key = PyBytes_FromStringAndSize(keystart,
- keylen);
+ key = _PyBytes_FromStringAndSize(keystart, keylen);
if (key == NULL)
goto error;
if (args_owned) {
@@ -1572,10 +1614,15 @@ _PyBytes_Concat(PyObject *a, PyObject *b)
goto done;
}
- result = PyBytes_FromStringAndSize(NULL, va.len + vb.len);
- if (result != NULL) {
- memcpy(PyBytes_AS_STRING(result), va.buf, va.len);
- memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);
+ if ((va.len + vb.len) != 0) {
+ result = bytes_alloc(va.len + vb.len);
+ if (result != NULL) {
+ memcpy(PyBytes_AS_STRING(result), va.buf, va.len);
+ memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);
+ }
+ }
+ else {
+ result = bytes_get_empty();
}
done:
@@ -1600,26 +1647,22 @@ _PyBytes_Repeat(PyObject *self, Py_ssize_t n)
"repeated bytes are too long");
return NULL;
}
- Py_ssize_t size = Py_SIZE(a) * n;
- if (size == Py_SIZE(a) && PyBytes_CheckExact(a)) {
+ Py_ssize_t old_size = Py_SIZE(a);
+ Py_ssize_t new_size = old_size * n;
+ if (new_size == old_size && PyBytes_CheckExact(a)) {
return Py_NewRef(a);
}
- size_t nbytes = (size_t)size;
- if (nbytes + PyBytesObject_SIZE <= nbytes) {
- PyErr_SetString(PyExc_OverflowError,
- "repeated bytes are too long");
- return NULL;
+
+ if (new_size == 0) {
+ return bytes_get_empty();
}
- PyBytesObject *op = PyObject_Malloc(PyBytesObject_SIZE + nbytes);
+
+ PyBytesObject *op = (PyBytesObject *)bytes_alloc(new_size);
if (op == NULL) {
return PyErr_NoMemory();
}
- _PyObject_InitVar((PyVarObject*)op, &PyBytes_Type, size);
- set_ob_shash(op, -1);
- op->ob_sval[size] = '\0';
-
- _PyBytes_RepeatBuffer(op->ob_sval, size, a->ob_sval, Py_SIZE(a));
+ _PyBytes_RepeatBuffer(op->ob_sval, new_size, a->ob_sval, old_size);
return (PyObject *) op;
}
@@ -1755,7 +1798,6 @@ bytes_subscript(PyObject *op, PyObject* item)
size_t cur;
const char* source_buf;
char* result_buf;
- PyObject* result;
if (PySlice_Unpack(item, &start, &stop, &step) < 0) {
return NULL;
@@ -1772,22 +1814,22 @@ bytes_subscript(PyObject *op, PyObject* item)
return Py_NewRef(self);
}
else if (step == 1) {
- return PyBytes_FromStringAndSize(
+ return _PyBytes_FromStringAndSize(
PyBytes_AS_STRING(self) + start,
slicelength);
}
else {
source_buf = PyBytes_AS_STRING(self);
- result = PyBytes_FromStringAndSize(NULL, slicelength);
- if (result == NULL)
+ PyObject* result = bytes_alloc(slicelength);
+ if (result == NULL) {
return NULL;
+ }
result_buf = PyBytes_AS_STRING(result);
for (cur = start, i = 0; i < slicelength;
cur += step, i++) {
result_buf[i] = source_buf[cur];
}
-
return result;
}
}
@@ -1843,7 +1885,7 @@ bytes___bytes___impl(PyBytesObject *self)
return Py_NewRef(self);
}
else {
- return PyBytes_FromStringAndSize(self->ob_sval, Py_SIZE(self));
+ return _PyBytes_FromStringAndSize(self->ob_sval, Py_SIZE(self));
}
}
@@ -2134,7 +2176,7 @@ do_xstrip(PyBytesObject *self, int striptype, PyObject
*sepobj)
return Py_NewRef(self);
}
else
- return PyBytes_FromStringAndSize(s+i, j-i);
+ return _PyBytes_FromStringAndSize(s+i, j-i);
}
@@ -2163,7 +2205,7 @@ do_strip(PyBytesObject *self, int striptype)
return Py_NewRef(self);
}
else
- return PyBytes_FromStringAndSize(s+i, j-i);
+ return _PyBytes_FromStringAndSize(s+i, j-i);
}
@@ -2468,15 +2510,15 @@ bytes_removeprefix_impl(PyBytesObject *self, Py_buffer
*prefix)
&& prefix_len > 0
&& memcmp(self_start, prefix_start, prefix_len) == 0)
{
- return PyBytes_FromStringAndSize(self_start + prefix_len,
- self_len - prefix_len);
+ return _PyBytes_FromStringAndSize(self_start + prefix_len,
+ self_len - prefix_len);
}
if (PyBytes_CheckExact(self)) {
return Py_NewRef(self);
}
- return PyBytes_FromStringAndSize(self_start, self_len);
+ return _PyBytes_FromStringAndSize(self_start, self_len);
}
/*[clinic input]
@@ -2507,15 +2549,15 @@ bytes_removesuffix_impl(PyBytesObject *self, Py_buffer
*suffix)
&& memcmp(self_start + self_len - suffix_len,
suffix_start, suffix_len) == 0)
{
- return PyBytes_FromStringAndSize(self_start,
- self_len - suffix_len);
+ return _PyBytes_FromStringAndSize(self_start,
+ self_len - suffix_len);
}
if (PyBytes_CheckExact(self)) {
return Py_NewRef(self);
}
- return PyBytes_FromStringAndSize(self_start, self_len);
+ return _PyBytes_FromStringAndSize(self_start, self_len);
}
/*[clinic input]
@@ -2955,7 +2997,7 @@ bytes_new_impl(PyTypeObject *type, PyObject *x, const
char *encoding,
PyErr_SetString(PyExc_ValueError, "negative count");
return NULL;
}
- bytes = _PyBytes_FromSize(size, 1);
+ bytes = _PyBytes_FromSizeZero(size);
}
}
else {
@@ -3144,13 +3186,14 @@ PyBytes_FromObject(PyObject *x)
* This allocator will be removed when ob_shash is removed.
*/
static PyObject *
-bytes_alloc(PyTypeObject *self, Py_ssize_t nitems)
+bytes_type_alloc(PyTypeObject *self, Py_ssize_t nitems)
{
+ // Initialize memory to zero
PyBytesObject *obj = (PyBytesObject*)PyType_GenericAlloc(self, nitems);
if (obj == NULL) {
return NULL;
}
- set_ob_shash(obj, -1);
+ set_ob_shash_unsafe(obj, -1);
return (PyObject*)obj;
}
@@ -3165,10 +3208,9 @@ bytes_subtype_new(PyTypeObject *type, PyObject *tmp)
n = PyBytes_GET_SIZE(tmp);
pnew = type->tp_alloc(type, n);
if (pnew != NULL) {
- memcpy(PyBytes_AS_STRING(pnew),
- PyBytes_AS_STRING(tmp), n+1);
- set_ob_shash((PyBytesObject *)pnew,
- get_ob_shash((PyBytesObject *)tmp));
+ memcpy(PyBytes_AS_STRING(pnew), PyBytes_AS_STRING(tmp), n+1);
+ set_ob_shash_unsafe((PyBytesObject*)pnew,
+ get_ob_shash((PyBytesObject *)tmp));
}
return pnew;
}
@@ -3269,7 +3311,7 @@ PyTypeObject PyBytes_Type = {
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
- bytes_alloc, /* tp_alloc */
+ bytes_type_alloc, /* tp_alloc */
bytes_new, /* tp_new */
PyObject_Free, /* tp_free */
.tp_version_tag = _Py_TYPE_VERSION_BYTES,
@@ -3375,6 +3417,7 @@ bytes_resize_inplace(PyObject **pv, Py_ssize_t newsize)
assert(Py_REFCNT(v) == 1);
assert(PyBytes_GET_SIZE(v) >= 1);
assert(newsize >= 1);
+ assert(get_ob_shash((PyBytesObject *)v) == -1);
// Only mutable bytes can be resized in-place
assert(_PyBytes_IsMutable(v));
@@ -3429,7 +3472,6 @@ int
_PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize)
{
PyObject *v = *pv;
- PyObject *result;
if (!PyBytes_Check(v) || newsize < 0) {
PyErr_BadInternalCall();
@@ -3445,7 +3487,8 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t
newsize)
}
if (oldsize == 0) {
- result = _PyBytes_FromSize(newsize, 0);
+ assert(newsize >= 1);
+ PyObject *result = bytes_alloc(newsize);
if (result == NULL) {
return -1;
}
@@ -3467,7 +3510,7 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t
newsize)
{
// Allocate and then copy so we don't get a shared immortal
// one-character singleton!
- result = _PyBytes_FromSize(newsize, 0);
+ PyObject *result = bytes_alloc(newsize);
if (!result) {
return -1;
}
@@ -3783,7 +3826,7 @@ byteswriter_check_consistency(PyBytesWriter *writer)
static inline int
byteswriter_resize(PyBytesWriter *writer, Py_ssize_t new_size, int resize)
{
- assert(new_size >= 0);
+ assert(new_size >= 1);
Py_ssize_t old_allocated = byteswriter_allocated(writer);
if (new_size <= old_allocated) {
@@ -3830,7 +3873,7 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t
new_size, int resize)
data = PyByteArray_AS_STRING(writer->obj);
}
else {
- writer->obj = PyBytes_FromStringAndSize(NULL, alloc);
+ writer->obj = bytes_alloc(alloc);
if (writer->obj == NULL) {
return -1;
}
@@ -4000,7 +4043,7 @@ PyBytesWriter_FinishWithSize(PyBytesWriter *writer,
Py_ssize_t size)
result = bytes_get_char((uint8_t)buffer[0]);
}
else {
- result = _PyBytes_FromSize(size, 0);
+ result = bytes_alloc(size);
if (result == NULL) {
goto error;
}
_______________________________________________
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]