compress: reuse the zstd compressor per thread - #10326
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #10326 +/- ##
==========================================
+ Coverage 87.65% 87.70% +0.04%
==========================================
Files 103 103
Lines 18716 18716
Branches 2881 2881
==========================================
+ Hits 16405 16414 +9
+ Misses 1609 1600 -9
Partials 702 702 ☔ View full report in Codecov by Harness. |
libzstd starts a worker pool when a compression context with nb_workers > 1 is set up and stops it again when that context is freed, so the one-shot zstd.compress() started and stopped the workers for every single chunk. Cache the compressor per thread instead (keyed by level and worker count), so its pool is built once and stays alive between chunks. Measured on the 2MiB chunks of the Silesia corpus with 4 workers on an M3 Pro: at zstd,-4 +20% throughput (2345 -> 2815 MB/s), at zstd,3 +9%. The compressed data does not change: set_pledged_input_size() plus FLUSH_FRAME produce exactly the bytes the one-shot API produced. Single-threaded compression goes through the same cache. That gains next to nothing (0.98 .. 1.07x), but keeps compression to a single code path. The price of caching is that a thread holds on to the contexts it used: +1.6MB peak RSS for a single-threaded level 3 context, +8MB for a 4 worker one. Compressors are stateful, so they are per thread, like the scratch buffer. In the child of a fork they are dropped, but deliberately not freed: the worker threads do not exist there, and freeing such a context would make libzstd wait for them forever (verified: without this, a child hangs as soon as it compresses). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThomasWaldmann
force-pushed
the
zstd-ctx-reuse
branch
from
September 3, 2026 19:27
38951ef to
6c2f23c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
libzstd starts a worker pool when a compression context with
nb_workers > 1is set up and stops it again when that context is freed. borg used the one-shotzstd.compress(), so that pool was built and torn down for every single chunk.This caches the compressor per thread (keyed by level and worker count), so the pool is created once and stays alive between chunks.
Measured
Full Silesia corpus, 2MiB chunks, 4 workers, through borg's own compressor, on an M3 Pro:
How much this is worth depends on how expensive thread creation is on the platform, so the gain will differ elsewhere.
Notes
set_pledged_input_size()+FLUSH_FRAMEproduce exactly the bytes the one-shot API produced; a test asserts this against a fresh one-shot compression, and the measured compression ratios are unchanged._decide()no longer branches.nb_workersis left unset for it, asnb_workers == 1would mean one asynchronous worker rather than single-threaded.Test plan
0 workers == 1), reused output == one-shot output over consecutive chunks for both the MT and the ST path, an 8-chunk concurrent roundtrip through one shared compressor instance, and below-threshold chunks caching a one-worker context.completion_cmd_test.pybash completion) also fail on master without this change.🤖 Generated with Claude Code