Skip to content

gh-160931: Speed up bytes construction from exact bytes and int - #160934

Open
cmaloney wants to merge 2 commits into
python:mainfrom
cmaloney:bytes_new_exact_first
Open

cmaloney wants to merge 2 commits into
python:mainfrom
cmaloney:bytes_new_exact_first

Conversation

@cmaloney

@cmaloney cmaloney commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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.

Could inline the PyLong as another if but that makes a bigger diff and the runtime is very close to just adding the !PyLong_CheckExact case.

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()")

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review skip news type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant