perf: pool compressor/decompressor contexts in the one-shot helpers - #3
Open
AskAlexSharov wants to merge 1 commit into
Open
perf: pool compressor/decompressor contexts in the one-shot helpers#3AskAlexSharov wants to merge 1 commit into
AskAlexSharov wants to merge 1 commit into
Conversation
AskAlexSharov
force-pushed
the
perf/pool-oneshot-helpers
branch
3 times, most recently
from
July 24, 2026 02:57
1d82c7f to
adde006
Compare
The convenience helpers (GzipCompress, ZlibCompress, GzipDecompress,
ZlibDecompress) allocated a fresh libdeflate context on every call and
freed it on return. Allocating a compressor builds internal match-finder
tables, so that alloc/free pair — not the compression itself — dominated
the cost of one-shot calls on small payloads.
Reuse contexts via bounded per-level pools. A bounded channel is used
rather than a sync.Pool: sync.Pool drops its contents on GC, which for a
C-backed handle means leaking the C context (no finalizer is attached on
this branch). A channel keeps pooled contexts reachable, and Put frees any
surplus beyond the cap, so memory stays bounded and nothing leaks. Each
handout is exclusively owned by one caller, so a pooled compressor is
never used concurrently (libdeflate contexts are not thread-safe).
Each pool is created lazily (one sync.Once per level). Capacity is the
exported PoolCapacity var: 0 (default) means auto = 2*GOMAXPROCS clamped
to [4, 32] so high-core machines don't retain many large (e.g. level-12)
contexts idle; a positive value is honored verbatim. Set it before first
use.
BenchmarkGzipCompressOneShot (Apple M4 Max, ~1 KB payload, level 6):
before after delta
Serial 8560 ns/op 6130 ns/op ~28% faster (116 -> 162 MB/s)
Parallel 1350 ns/op 965 ns/op ~29% faster (733 -> 1029 MB/s)
Go allocs/op also drop from 2 to 1 (the reused wrapper is no longer
heap-allocated per call). Adds a concurrent round-trip test across levels
(run under -race), a PoolCapacity override test, and the benchmarks.
AskAlexSharov
force-pushed
the
perf/pool-oneshot-helpers
branch
from
July 24, 2026 03:02
adde006 to
03a706d
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.
Inefficiency
The convenience helpers —
GzipCompress,ZlibCompress,GzipDecompress,ZlibDecompress— allocated a fresh libdeflate context on every call and freed it on return:libdeflate_alloc_compressorallocates internal match-finder tables, so for one-shot calls on small payloads that alloc/free pair — not the compression — dominates the cost. (The low-level API already lets callers reuse a context; the README even recommends pooling. This PR brings the same benefit to the convenience API.)Change
Reuse contexts via bounded, per-level pools (
pool.go). The helpers nowgetCompressor/putCompressor(and the decompressor equivalents) instead of allocating fresh each time.sync.Pool:sync.Pooldrops its contents during GC. For a C-backed handle that means leaking the C context, since this branch attaches no finalizer to free it. A channel keeps the pooled contexts reachable, andPutfrees any surplus beyond the cap — so memory stays bounded and nothing leaks. Each handout is owned exclusively by one caller, so a pooled compressor is never used concurrently (libdeflate contexts are not thread-safe).sync.Onceper level, so a program that only compresses at a single level allocates only that level's pool.Configurable capacity
The auto value is clamped to a ceiling of 32 so high-core machines don't retain a large number of big (e.g. level-12) contexts idle. An explicit
PoolCapacityis honored verbatim. Set it before the first compress/decompress call: it does not resize pools that already exist and is not safe to change concurrently with use.Benchmark
BenchmarkGzipCompressOneShot, Apple M4 Max, ~1 KB payload, level 6,-count=3:Go allocs/op also drop 2 → 1 (the reused wrapper is no longer heap-allocated per call).
Tests
TestConvenienceHelpersConcurrent— 32 goroutines round-tripping across levels 1/6/12, run under-race.TestPoolCapacityOverride— thePoolCapacityoverride path.gofmt,go vet,golangci-lint(v2), andgo test -raceare all clean.Scope
Based on
main, independent of #2. Both touchlibdeflate.go, but in different regions (the bug fixes are in the methods; this PR only rewires the four convenience functions), so they should merge without conflict in either order.