You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Chunks are typically ~2 MiB, so each malloc/free pair is large enough that the allocator hands the pages back to the OS and faults them in again for the next chunk. In a profile this shows up as madvise plus the zeroing/copying around it, once per chunk, on both create and extract.
borg already has the right tool for this: helpers/datastruct.py:Buffer is a managed, resizable, reusable buffer with an optional limit, and compress.pyx uses it for the lz4 path (buffer = Buffer(bytearray, size=0)). The AEAD layer just does not use it.
So roughly 2% of cpu on each path, which is the single largest borg-owned item inside the memory-management bucket. (The largest item overall in extract is zstd's own output assembly at ~6%, but that is inside the Python zstd binding's _OutputBuffer and the stdlib API has no decompress_into, so it is not ours to fix.)
For context, the full memory-management breakdown for create, as % of total cpu:
Buffer grows monotonically and never shrinks, so a limit should be set; the natural bound is MAX_DATA_SIZE plus the AEAD overhead.
Two related-but-separate observations from the same profile, not worth their own issues yet: a fresh ZstdCompressor/ZstdDecompressor context is created per chunk (~0.6% create / ~1.2% extract - I measured reuse and it gives no throughput gain at 2 MiB chunks, only removes the churn), and PyBuffer_ToContiguous -> bytes() costs 1.48% of create from a call site I have not pinned down yet.
All numbers are from one machine (Apple M3 Pro, 12 cores) with one workload shape. On already-compressed data the profile shifts a lot, since zstd bails out early and everything else grows proportionally.
The AEAD encrypt/decrypt paths allocate and free a fresh output buffer for every chunk:
https://github.com/borgbackup/borg/blob/master/src/borg/crypto/low_level.pyx
Chunks are typically ~2 MiB, so each malloc/free pair is large enough that the allocator hands the pages back to the OS and faults them in again for the next chunk. In a profile this shows up as
madviseplus the zeroing/copying around it, once per chunk, on both create and extract.borg already has the right tool for this:
helpers/datastruct.py:Bufferis a managed, resizable, reusable buffer with an optional limit, andcompress.pyxuses it for the lz4 path (buffer = Buffer(bytearray, size=0)). The AEAD layer just does not use it.How much is it worth
Profiled on master (f8dee0c), 6 GiB compressible file,
zstd,3/aes256-ocb/sha256ids, default (fastcdc) chunker, macOS/arm64, native sampling, idle samples excluded:So roughly 2% of cpu on each path, which is the single largest borg-owned item inside the memory-management bucket. (The largest item overall in extract is zstd's own output assembly at ~6%, but that is inside the Python zstd binding's
_OutputBufferand the stdlib API has nodecompress_into, so it is not ours to fix.)For context, the full memory-management breakdown for create, as % of total cpu:
and extract:
Notes / caveats
Buffergrows monotonically and never shrinks, so alimitshould be set; the natural bound isMAX_DATA_SIZEplus the AEAD overhead.ZstdCompressor/ZstdDecompressorcontext is created per chunk (~0.6% create / ~1.2% extract - I measured reuse and it gives no throughput gain at 2 MiB chunks, only removes the churn), andPyBuffer_ToContiguous -> bytes()costs 1.48% of create from a call site I have not pinned down yet.🤖 Generated with Claude Code