Uh oh!
There was an error while loading. Please reload this page.
gh-139871: Optimize bytearray unique bytes iconcat - #141862
Conversation
If the bytearray is empty and a uniquely referenced bytes object is
being concatenated (ex. one just recieved from read), just use its
storage as the backing for the bytearray rather than copying it.
build_bytes_unique: Mean +- std dev: [base] 383 ns +- 11 ns -> [iconcat_opt] 342 ns +- 5 ns: 1.12x faster
build_bytearray: Mean +- std dev: [base] 496 ns +- 8 ns -> [iconcat_opt] 471 ns +- 13 ns: 1.05x faster
encode: Mean +- std dev: [base] 482 us +- 2 us -> [iconcat_opt] 13.8 us +- 0.1 us: 34.78x faster
Benchmark hidden because not significant (1): build_bytes
Geometric mean: 2.53x faster
note: Performance of build_bytes is expected to stay constant.
```python
import pyperf
runner = pyperf.Runner()
count1 = 1_000
count2 = 100
count3 = 10_000
CHUNK_A = b'a' * count1
CHUNK_B = b'b' * count2
CHUNK_C = b'c' * count3
def build_bytes():
# Bytes not uniquely referenced.
ba = bytearray()
ba += CHUNK_A
ba += CHUNK_B
ba += CHUNK_C
def build_bytes_unique():
ba = bytearray()
# Repeat inline results in uniquely referenced bytes.
ba += b'a' * count1
ba += b'b' * count2
ba += b'c' * count3
def build_bytearray():
# Each bytearray appended is uniquely referenced.
ba = bytearray()
ba += bytearray(CHUNK_A)
ba += bytearray(CHUNK_B)
ba += bytearray(CHUNK_C)
runner.bench_func('build_bytes', build_bytes)
runner.bench_func('build_bytes_unique', build_bytes_unique)
runner.bench_func('build_bytearray', build_bytearray)
runner.timeit(
name="encode",
setup="a = 'a' * 1_000_000",
stmt="bytearray(a, encoding='utf8')")
```Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Co-authored-by: Victor Stinner <vstinner@python.org>
Uh oh!
There was an error while loading. Please reload this page.
encukou
commented
Nov 25, 2025
Here's a test that should pass, but doesn't: // make some bytesPyObject*bytes=PyBytes_FromString("aaB");
assert(bytes);
// make an empty bytearrayPyObject*ba=PyByteArray_FromStringAndSize("", 0);
assert(ba);
// append bytes to bytearray (in place, getting a new reference)PyObject*new_ba=PySequence_InPlaceConcat(ba, bytes);
assert(new_ba==ba);
Py_DECREF(new_ba);
// pop from bytearrayPy_DECREF(PyObject_CallMethod(ba, "pop", ""));
// check that our bytes was not modifiedassert(memcmp(PyBytes_AsString(bytes), "aaB", 3) ==0);
Py_DECREF(bytes);
Py_DECREF(ba);AFAIK, you need to use |
| PyObject *taken = PyObject_CallMethodNoArgs(other, | ||
| &_Py_ID(take_bytes)); |
There was a problem hiding this comment.
This looks unsafe to me. If you call a method, you may invalidate the assumptions you verified earlier
There was a problem hiding this comment.
Maybe call bytearray_take_bytes_impl() directly to reduce the risk of side effects? And you can check again _PyObject_IsUniquelyReferenced() in an assertion.
cmaloney
commented
Nov 29, 2025
(Iterating on this locally; should have updates next week) |
Co-authored-by: Victor Stinner <vstinner@python.org>
cmaloney
commented
Dec 3, 2025
@encukou : When I try using PyUnstable_Object_IsUniqueReferencedTemporary then use I was modeling this off of I don't see a way to do the generalized optimization currently; still think there should be a way just not sure the path and suspect it's a number of steps. I'll probably close this PR and open one for just the encoding case ( |
colesbury
commented
Dec 3, 2025
That seems like a bug in Lines 2758 to 2767 in c0c6514 |
cmaloney
commented
Dec 3, 2025
Created GH-142243 doing just the |
cmaloney
commented
Dec 3, 2025
Tested with |
This PR is stale because it has been open for 30 days with no activity. |
If the bytearray is empty and a uniquely referenced bytes object is being concatenated (ex. one just received from read), just use its storage as the backing for the bytearray rather than copying it. The bigger the bytes the bigger the saving.
build_bytes_unique: Mean +- std dev: [base] 383 ns +- 11 ns -> [iconcat_opt] 342 ns +- 5 ns: 1.12x faster
build_bytearray: Mean +- std dev: [base] 496 ns +- 8 ns -> [iconcat_opt] 471 ns +- 13 ns: 1.05x faster
encode: Mean +- std dev: [base] 482 us +- 2 us -> [iconcat_opt] 13.8 us +- 0.1 us: 34.78x faster
Benchmark hidden because not significant (1): build_bytes
Geometric mean: 2.53x faster
note: Performance of build_bytes is expected to stay constant.
From my understanding of reference counting I think this is safe to do for
iconcat(and would be safe to do forba[:] = b'\0' * 1000discuss topic). The briefly refcount 2 isn't ideal but I think good enough for the performance delta. I'm hoping if I can ship an implementation of gh-87613 can do the same optimization forbytearray(b'\0' * 4096).If the iconcat refcount 2 part isn't good, can tweak to keep the
enecode+bytearrayperformance improvement without changingiconcatgenerally.cc: @vstinner , @encukou
.take_bytes([n])a zero-copy path frombytearraytobytes#139871