Skip to content

Commit 7c2bb06

Browse files
cmaloneyclaude
andcommitted
gh-160931: Speed up bytes construction from exact bytes and int
Since 3.11 added `bytes.__bytes__`, bytes(b) for an exact bytes binds and calls that method only to get b back. Return an exact bytes argument directly, like PyObject_Bytes() and PyBytes_FromObject() do, and skip the lookup, which always misses, for an exact int. How bytes(x) resolves is unchanged: both types are immutable, so the skipped lookup and call could only ever produce the same result, and subclasses, including bool, still look up `__bytes__` first. These cases just get there faster. pyperf, PGO+LTO, interleaved A/B on 19270d3 (A/A spread <= 1.05x): | Benchmark | main | patch | |-------------------|:-------:|:---------------------:| | bytes(b) | 28.5 ns | 12.8 ns: 2.22x faster | | Sub(b) | 51.7 ns | 36.0 ns: 1.44x faster | | bytes(16) | 23.5 ns | 21.0 ns: 1.12x faster | | bytes(hb) | 38.7 ns | 38.0 ns: 1.02x faster | | bytes(s, 'ascii') | 26.5 ns | 27.1 ns: 1.02x slower | | bytes() | 10.7 ns | 11.1 ns: 1.03x slower | Not significant: bytes(ba), bytes(l), bytes(mv). The small slowdowns (1.02-1.03x) are within the noise on my machine. import pyperf runner = pyperf.Runner() for stmt in ["bytes(b)", "bytes(16)", "bytes(ba)", "bytes(mv)", "bytes(l)", "bytes(s, 'ascii')", "bytes(hb)", "Sub(b)", "bytes()"]: runner.timeit(stmt, stmt, "b = b'abcdefghijklmnop'; ba = bytearray(b); mv = memoryview(b); " "l = list(range(16)); s = 'abcdefghijklmnop'\n" "class Sub(bytes): pass\n" "class HB:\n def __bytes__(self): return b\n" "hb = HB()") Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ef1fce0 commit 7c2bb06

2 files changed

Lines changed: 10 additions & 2 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` construction from an exact :class:`bytes` or
2+
:class:`int` argument by skipping the :meth:`~object.__bytes__` lookup.

‎Objects/bytesobject.c‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2970,10 +2970,16 @@ bytes_new_impl(PyTypeObject *type, PyObject *x, const char *encoding,
29702970
"errors without a string argument");
29712971
return NULL;
29722972
}
2973+
/* bytes is immutable and bytes.__bytes__() returns an exact bytes
2974+
unchanged, so skip the lookup and the call. */
2975+
else if (PyBytes_CheckExact(x)) {
2976+
bytes = Py_NewRef(x);
2977+
}
29732978
/* We'd like to call PyObject_Bytes here, but we need to check for an
29742979
integer argument before deferring to PyBytes_FromObject, something
2975-
PyObject_Bytes doesn't do. */
2976-
else if ((func = _PyObject_LookupSpecial(x, &_Py_ID(__bytes__))) != NULL) {
2980+
PyObject_Bytes doesn't do. int is immutable and has no __bytes__. */
2981+
else if (!PyLong_CheckExact(x) &&
2982+
(func = _PyObject_LookupSpecial(x, &_Py_ID(__bytes__))) != NULL) {
29772983
bytes = _PyObject_CallNoArgs(func);
29782984
Py_DECREF(func);
29792985
if (bytes == NULL)

0 commit comments

Comments
 (0)