Skip to content

Improve memory usage and performance for tobytes() - #9938

Open
akx wants to merge 5 commits into
python-pillow:mainfrom
akx:tobytes-buf-size
Open

Improve memory usage and performance for tobytes()#9938
akx wants to merge 5 commits into
python-pillow:mainfrom
akx:tobytes-buf-size

Conversation

@akx

@akx akx commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Closes #9220 (supersedes it).

This is an extension of my comment on #9220 (comment):

  • Encoders, both C and Python, now talk a new optimal_bufsize property, the number of bytes needed in a byte buffer to encompass the whole encoding.
  • Image.tobytes() now checks the optimal bufsize after it initializes the encoder, and passes it through if known; otherwise, the same old logic (the larger of MAXBLOCK, or the memory required to encode a single 8-bit RGBA row) is used.
    • With an exact buffer size passed in, RAW encoding only requires a single allocation, not N allocations of the chunk size plus the accumulation to join into a buffer (which is also work). In memory-constrained situations, the last would have failed anyway, only unfortunately after all of the encoding work had been done.

tobytes() is being used in surprising places around Pillow (see the changelog fragment), so this should have pleasant effects all around...

@akx

akx commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Oh, and for proof, with this reproducer gist:

(main) $ uv pip install -q -e . && time uv run rss_repro.py
revision: 23cbae1d6fbf017855ea40d2421a33fb2e503899
build:    no optimal_bufsize -- MAXBLOCK chunks and a join
platform: darwin, python 3.14.6, mode RGB

        size       result     peak RSS   over baseline   ratio
--------------------------------------------------------------
   2000x2000     11.4 MiB     64.4 MiB        26.6 MiB   2.33x
   4000x4000     45.8 MiB    191.2 MiB       107.9 MiB   2.36x
   8000x8000    183.1 MiB    700.4 MiB       433.2 MiB   2.37x
   20000x500     28.6 MiB    127.7 MiB        67.1 MiB   2.34x

________________________________________________________
Executed in    1.69 secs    fish           external
   usr time    1.13 secs    0.11 millis    1.13 secs
   sys time    0.51 secs    3.88 millis    0.51 secs

and

(tobytes-buf-size) $ uv pip install -q -e . && time uv run rss_repro.py
revision: de42638fe246080aefb616f2dfbb9a12d125c0db
build:    optimal_bufsize present, reports 768 for (16, 16) RGB
platform: darwin, python 3.14.6, mode RGB

        size       result     peak RSS   over baseline   ratio
--------------------------------------------------------------
   2000x2000     11.4 MiB     49.1 MiB        11.6 MiB   1.01x
   4000x4000     45.8 MiB    129.2 MiB        45.7 MiB   1.00x
   8000x8000    183.1 MiB    450.5 MiB       183.7 MiB   1.00x
   20000x500     28.6 MiB     89.7 MiB        28.8 MiB   1.01x

________________________________________________________
Executed in    1.52 secs    fish           external
   usr time    1.08 secs    0.11 millis    1.08 secs
   sys time    0.40 secs    3.88 millis    0.39 secs

@codspeed-hq

codspeed-hq Bot commented Sep 1, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by ×2

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 4 improved benchmarks
✅ 599 untouched benchmarks
⏩ 335 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_pack[1237x811-RGBA] 9.7 ms 3.9 ms ×2.5
test_pack[1237x811-L] 2.4 ms 1 ms ×2.3
test_pack[1237x811-LA] 6.3 ms 3.6 ms +73.95%
test_pack[1237x811-RGB] 9.8 ms 5.8 ms +69.47%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing akx:tobytes-buf-size (de42638) with main (c3078e7)2

Open in CodSpeed

Footnotes

  1. 335 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (23cbae1) during the generation of this report, so c3078e7 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@akx
akx marked this pull request as ready for review September 1, 2026 16:01
@akx

This comment was marked as outdated.

Comment thread src/encode.c
Py_ssize_t bufsize = row * state->ysize;
// Cap the size to the whole number of rows that fits an `int`.
// TODO: this needs to be changed when encoders' buffer sizes become `ssize_t`,
// like decoders did in ca1cf5925.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't referring to an existing PR, right? You're presuming it will happen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No existing PR, it's just a note for a future implementer, be that me or someone else 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants