Skip to content

Decompress/compress output buffer growth via _PyBytes_Resize causes avoidable copies and heap-layout-dependent performance #256

Description

@Dreamsorcerer

Summary

isal_zlib grows its output in a single PyBytes object that is doubled with _PyBytes_Resize() (i.e. realloc) as output accumulates:

Decompressing a 512 KiB payload reallocs the buffer 6 times (16K→32K→…→1M when the buffer fills exactly, plus the final shrink resize). Each step is a glibc realloc that either extends in place or moves+copies depending on what happens to neighbour the chunk, so:

  1. Avoidable copy traffic — up to ~1.5 MiB memcpy per 512 KiB decompressed in the worst case, and
  2. Nondeterministic performance — the copy volume depends on heap layout, i.e. on everything the process allocated before the call.

Testing against aiohttp's benchmark:

backend cycle spread
stdlib zlib 0.03%
isal.isal_zlib 37.4% (22.9M–33.1M cycles)

The entire delta between runs localizes to the memmove blob behind glibc realloc (ISA-L's own internal memmoves are bit-identical across runs, so this is purely the Python-side buffer management, not ISA-L).

Suggested fix

CPython removed exactly this pattern from zlibmodule.c/bz2/lzma in python/cpython#85658 (bpo-41486), merged as python/cpython#21740 for 3.10: _BlocksOutputBuffer keeps a list of bytes blocks and joins once at the end — "no overhead of resizing the buffer" — with measured speedups on top of the determinism. The header is self-contained and copyable: Include/internal/pycore_blocks_output_buffer.h, current usage in Modules/zlibmodule.c.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions