Reduce LZMA Allocations#1335
Conversation
There was a problem hiding this comment.
Pull request overview
This PR reduces GC pressure in LZMA/LZMA2 encode/decode paths by switching to ArrayPool for several hot buffers (match finder buffers, LZMA2 chunk buffer, async header buffers, single-byte range coder buffers), making the corresponding classes disposable, and refactoring Lzma2EncoderStream to reuse a single Encoder instance and cached properties. It also wires a new SharpCompress.AotSmoke test project into the solution and makes the Performance program async.
Changes:
- Use
ArrayPool(andstackalloc/Stream.Null) inLzInWindow,LzBinTree,LzmaStream(async header),LZipStream.Async,RangeCoder(single-byte buffers), andLzma2EncoderStream, with matchingDisposecleanup. - Refactor
Lzma2EncoderStreamto reuse oneEncoder, cachePropertiesand the LZMA props byte, and pass(Buffer, Length)tuples to avoid extra array copies; addEncoder.Disposeand an internalSpan-basedSetDecoderProperties. - Add
SharpCompress.AotSmokeproject toSharpCompress.slnand convertPerformance/Program.cstoasync Task Main, switching the profiler sample toSevenZipBenchmarks.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| SharpCompress.sln | Adds AotSmoke project entries and config rows. |
| src/SharpCompress/packages.lock.json | Adjusts ILLink.Tasks versions (downgraded). |
| src/SharpCompress/Compressors/LZMA/LZ/LzBinTree.cs | Rents/returns _son/_hash via ArrayPool and overrides Dispose. |
| src/SharpCompress/Compressors/LZMA/LZ/LzInWindow.cs | Pools _bufferBase and implements IDisposable. |
| src/SharpCompress/Compressors/LZMA/LzmaEncoder.cs | Implements IDisposable to dispose the match finder. |
| src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs | Adds ReadOnlySpan<byte> overload of SetDecoderProperties. |
| src/SharpCompress/Compressors/LZMA/LzmaStream.cs | Adds pooled async header buffer and disposes encoder/buffer. |
| src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs | Uses the pooled 6-byte header buffer for chunk header reads. |
| src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs | Uses ArrayPool for the LZIP header with try/finally. |
| src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs | Reuses one Encoder, pools buffers, passes lengths explicitly, refactors writes. |
| src/SharpCompress/Compressors/LZMA/RangeCoder/RangeCoder.cs | Adds _singleByteBuffer field; removes dead comments. |
| src/SharpCompress/Compressors/LZMA/RangeCoder/RangeCoder.Async.cs | Reuses a single-byte buffer across async reads/writes. |
| tests/SharpCompress.Performance/Program.cs | Switches Main to async; samples SevenZipBenchmarks for profiling. |
Comments suppressed due to low confidence (1)
src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs:203
- If
FlushChunkor_output.WriteByte(0x00)throws, the rented_bufferand_encoder's internal pooled buffers are never released, leaking ArrayPool buffers. Consider wrapping the cleanup (_encoder.Dispose()andArrayPool<byte>.Shared.Return(_buffer)) in atry/finallyso the pooled resources are always returned even on failure during the final flush. The same issue applies toDisposeAsyncbelow.
protected override void Dispose(bool disposing)
{
if (disposing && !_isDisposed)
{
_isDisposed = true;
// Flush remaining buffered data
if (_bufferPosition > 0)
{
FlushChunk();
}
// Write LZMA2 end marker
_output.WriteByte(0x00);
_encoder.Dispose();
ArrayPool<byte>.Shared.Return(_buffer);
}
base.Dispose(disposing);
}
private static readonly byte[] _endMarker = [0x00];
#if !LEGACY_DOTNET || NETSTANDARD2_1
public override async ValueTask DisposeAsync()
#else
public async ValueTask DisposeAsync()
#endif
{
if (!_isDisposed)
{
_isDisposed = true;
if (_bufferPosition > 0)
{
await FlushChunkAsync().ConfigureAwait(false);
}
#if !LEGACY_DOTNET || NETSTANDARD2_1
await _output.WriteAsync(new Memory<byte>(_endMarker)).ConfigureAwait(false);
#else
await _output.WriteAsync(_endMarker, 0, 1).ConfigureAwait(false);
#endif
_encoder.Dispose();
ArrayPool<byte>.Shared.Return(_buffer);
}
#if !LEGACY_DOTNET || NETSTANDARD2_1
await base.DisposeAsync().ConfigureAwait(false);
#endif
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Optimal is now a struct array, removing 4096 per-encoder object allocations.
- Encoder literal models are flattened into one pooled contiguous BitEncoder[].
- Decoder literal models are flattened into one non-pooled contiguous BitDecoder[].
- Updated async decoder paths to use the flattened model base index.
- Fixed LzBinTree pooled buffer nullability cleanup.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs:338
- This comment still refers to a “fresh LZMA encoder” per chunk, but the code now reuses a single encoder instance. Either re-instantiate the encoder per chunk or (preferably) adjust the comment to describe the actual reset behavior.
// Each chunk is compressed independently with a fresh LZMA encoder,
// so we must use 0xE0 (full reset: dictionary + state + properties) every time.
// The decoder uses outWindow.Total for literal context and posState;
// 0xE0 triggers outWindow.Reset() which zeros Total, matching the encoder's
// assumption that position starts at 0 for each chunk.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
# Conflicts: # SharpCompress.sln
This pull request introduces significant memory optimizations across the LZMA/LZMA2 compression code by switching from direct array allocations to using
ArrayPoolfor buffer management. Additionally, it updates resource cleanup logic to ensure pooled buffers are properly returned, and refactors the LZMA2 encoder stream for efficiency and clarity. The solution file is also updated to include a new test project.Memory pooling and buffer management improvements:
ArrayPoolinLzBinTree,LzInWindow, andLzma2EncoderStream, and ensured all buffers are returned to the pool during disposal to reduce GC pressure and improve performance. (src/SharpCompress/Compressors/LZMA/LZ/LzBinTree.cs,src/SharpCompress/Compressors/LZMA/LZ/LzInWindow.cs,src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs, [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]Lzma2EncoderStreamto use pooled buffers for headers and compressed data, with proper cleanup in finally blocks. (src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs, src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.csL344-R382)Resource management and disposal:
Disposemethods in relevant classes to ensure all pooled resources are released and streams are properly closed. (src/SharpCompress/Compressors/LZMA/LZ/LzBinTree.cs,src/SharpCompress/Compressors/LZMA/LZ/LzInWindow.cs,src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs, [1] [2] [3] [4]LZMA2 encoder refactoring:
Lzma2EncoderStreamto reuse a singleEncoderinstance, cache properties, and streamline chunk compression logic for improved efficiency and maintainability. (src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs, [1] [2] [3] [4]Test infrastructure:
SharpCompress.AotSmoketest project to the solution and updated configuration sections accordingly. (SharpCompress.sln, [1] [2] [3]Async I/O improvements:
src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs, [1] [2]src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs, [3]These changes collectively improve memory efficiency, reduce allocations, and make the codebase more robust in high-throughput or resource-constrained environments.