Repository navigation
gh-160310: Fix quadratic one-shot decompress() with many members - #160311
Conversation
gzip.decompress(), bz2.decompress(), lzma.decompress() and compression.zstd.decompress() passed all of the remaining input to a new decompressor for every member and copied the rest back via unused_data. Feed each member in chunks of increasing size instead.
gpshead
left a comment
There was a problem hiding this comment.
A high level comment I have on this PR... could or should we meaningfully factor out the common shape memoryview loop code used across multiple decompressors?
I think yes, seems reasonable for |
…ommon bz2.decompress(), lzma.decompress() and compression.zstd.decompress() had three copies of the chunked multi-stream loop. Move it into compression._common._streams._decompress_all(), which takes the decompressor factory, the trailing-data error, the stream magic and the truncation error. gzip keeps its own loop.
|
Thanks @gpshead for the PR 🌮🎉.. I'm working now to backport this PR to: 3.14, 3.15. |
|
Sorry, @gpshead, I could not cleanly backport this to Please backport manually with cherry_picker, see the devguide for more information. |
|
Sorry, @gpshead, I could not cleanly backport this to Please backport manually with cherry_picker, see the devguide for more information. |
|
GH-161037 is a backport of this pull request to the 3.15 branch. |
|
GH-161038 is a backport of this pull request to the 3.14 branch. |
…bers (GH-160311) (#161038) gzip.decompress(), bz2.decompress(), lzma.decompress() and compression.zstd.decompress() handed each new decompressor all of the remaining input and copied the rest back through unused_data, so input made of many concatenated members took quadratic time. Each member is now fed in chunks that start at 1 KiB and double, and the loop steps back over unused_data by offset. The first chunk is the whole input, so single-member data still takes one decompressor call. bz2, lzma and zstd share the loop through a private helper in compression._common._streams; gzip keeps its own because it parses the member header, trailer and padding itself. ANT-2026-9R8SO5S2 (cherry picked from commit e80962a)
…bers (GH-160311) (#161037) gzip.decompress(), bz2.decompress(), lzma.decompress() and compression.zstd.decompress() handed each new decompressor all of the remaining input and copied the rest back through unused_data, so input made of many concatenated members took quadratic time. Each member is now fed in chunks that start at 1 KiB and double, and the loop steps back over unused_data by offset. The first chunk is the whole input, so single-member data still takes one decompressor call. bz2, lzma and zstd share the loop through a private helper in compression._common._streams; gzip keeps its own because it parses the member header, trailer and padding itself. ANT-2026-9R8SO5S2 (cherry picked from commit e80962a)
The one-shot
decompress()functions ofgzip,bz2,lzmaandcompression.zstdgave each new decompressor all of the remaining input and copied the rest back throughunused_data, so input with many members took quadratic time. They now feed each member in chunks that start at 1 KiB and double, and step back overunused_databy offset; the first chunk is the whole input, so single-member data still takes one decompressor call.bz2,lzmaandzstdshare this loop through a private helper incompression._common._streams, whilegzipkeeps its own because it parses the member header and trailer itself. Results are unchanged for bytes-like input, but the three shared ones now slice amemoryviewof the argument, so a non-contiguousmemoryviewraisesTypeErrorwhere it raisedBufferError, and theTypeErrorfor a non-buffer argument has a different message.Fixes #160310
ANT-2026-9R8SO5S2
decompress()ingzip,bz2,lzmaandcompression.zstdis quadratic in the number of members #160310