ManagedCompressions.Zstd 0.2.0

dotnet add package ManagedCompressions.Zstd --version 0.2.0
                    
NuGet\Install-Package ManagedCompressions.Zstd -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ManagedCompressions.Zstd" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ManagedCompressions.Zstd" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="ManagedCompressions.Zstd" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ManagedCompressions.Zstd --version 0.2.0
                    
#r "nuget: ManagedCompressions.Zstd, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ManagedCompressions.Zstd@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ManagedCompressions.Zstd&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=ManagedCompressions.Zstd&version=0.2.0
                    
Install as a Cake Tool

ManagedCompressions.Zstd

Zstandard (RFC 8878) compression and decompression for .NET, in fully managed C#.

  • It contains no native code: no bundled library and no P/Invoke, so it runs wherever .NET runs, and it is compatible with trimming and Native AOT.
  • It writes the same frames as libzstd. At every compression level, ManagedCompressions.Zstd writes exactly the bytes that libzstd 1.5.7 writes, with dictionaries, with long-distance matching and in multithreaded compression.
  • It is fast. On arm64, decompression is 1.3–1.5× as fast as the libzstd that NativeCompressions and .NET ship, and 1.11–1.21× as fast as libzstd built from source at -O3 on the Silesia corpus. On x64, where libzstd has a hand-written assembly Huffman decoder, it is 1.0–1.2× as fast as NativeCompressions' libzstd. Compression of large inputs is within a few percent of libzstd, except on x64 at levels 1 to 5, which are up to 13 % slower (see Performance).
  • A warm compressor or decompressor allocates nothing.

ManagedCompressions.Zstd is an independent implementation of the format. It is not affiliated with the zstd project or with Meta.

Requirements

ManagedCompressions.Zstd targets .NET 11 (net11.0). Its only dependency is System.IO.Hashing, for the XXH64 frame checksums.

dotnet add package ManagedCompressions.Zstd

From source, reference src/ManagedCompressions.Zstd/ManagedCompressions.Zstd.csproj.

Usage

Decompression

using System.Buffers;
using ManagedCompressions.Zstd;

var decompressor = new ZstdDecompressor();   // reuse it: it keeps its tables and buffers between frames

if (ZstdDecompressor.TryGetFrameContentSize(frame, out ulong size))
{
    byte[] content = new byte[size];
    OperationStatus status = decompressor.Decompress(frame, content, out int consumed, out int written);
}

Decompress decodes one whole frame per call:

status meaning
Done The frame was decoded. consumed is its size, so the next frame starts there.
NeedMoreData The source ends inside the frame.
DestinationTooSmall The content does not fit in the destination.
InvalidData The source is not a valid frame.

After NeedMoreData or DestinationTooSmall, nothing has been consumed, so call again with the whole frame or a larger destination. Checksums are verified when a frame has one. A skippable frame decodes to nothing.

Compression

var compressor = new ZstdCompressor(level: 3);   // from ZstdCompressor.MinLevel (-131072) to MaxLevel (22)

byte[] frame = new byte[ZstdCompressor.GetMaxCompressedLength(content.Length)];
compressor.Compress(content, frame, out _, out int length);   // the frame is frame[..length]

Compress writes one whole frame per call, and the frame always declares its content size. Set AppendChecksum = true to end each frame with a checksum of its content. Level 0 selects ZstdCompressor.DefaultLevel, which is 3.

Dictionaries

byte[] dictionary = File.ReadAllBytes("records.dict");   // trained by `zstd --train`, or any raw content

var compressor = new ZstdCompressor(level: 3, dictionary);
var decompressor = new ZstdDecompressor(dictionary);

The dictionary is prepared once, in the constructor, and then used for every frame. ManagedCompressions.Zstd does not train dictionaries, so use zstd --train, or ZstandardDictionary.Train from System.IO.Compression.

A decompressor can also take the dictionary with each frame, which lets one decompressor serve frames of any dictionary:

var decompressor = new ZstdDecompressor();
OperationStatus status = decompressor.Decompress(frame, content, dictionary, out int consumed, out int written);

The dictionary is copied for the call and not kept, but its tables are kept for the next calls that pass the same dictionary. From then on, a frame costs a copy of its dictionary and nothing more, as if the decompressor had been created with it. Another dictionary rebuilds the tables without allocating.

Parallel compression

int length = await compressor.CompressAsync(content, frame, maxDegreeOfParallelism: 8);

CompressAsync cuts the source into jobs and compresses them on the thread pool. It writes the same frame as libzstd with worker threads (ZSTD_c_nbWorkers), so the frame does not depend on the degree of parallelism. Up to 512 KiB, it writes the same frame as Compress.

Threading

Instances are not thread-safe. Use one per thread, or one per concurrent operation.

Compatibility with libzstd

Decompression follows libzstd's decoding and validation, so the two accept and reject the same frames. ManagedCompressions.Zstd also enforces RFC 8878's limit on the size of a block in every case, where libzstd only partly enforces it in one-shot decoding.

Compression reproduces libzstd 1.5.7's ZSTD_compress byte for byte, at every level from -131072 to 22. This covers:

  • the nine strategies, from fast to btultra2
  • the pre- and post-block splitters
  • the long-distance matching that libzstd turns on at level 22 for sources over 64 MiB

With a dictionary, ManagedCompressions.Zstd writes the frames libzstd writes when the dictionary is prepared at the same level (ZSTD_createCDict, then ZSTD_CCtx_refCDict), which is also how the ZstandardDictionary of System.IO.Compression uses it. CompressAsync reproduces libzstd's multithreaded compression (zstdmt), dictionaries and long-distance matching included.

The limitations are these:

  • There is no streaming API. A frame is compressed or decompressed in one call, from and into memory.
  • Sources and frames must fit in a span, so they are limited to 2 GiB.
  • Compression takes a level, a dictionary and the checksum option. libzstd's advanced parameters, such as the window size or the strategy, are not exposed.

Performance

speedup over libzstd 1.5.7 ManagedCompressions.Zstd .NET 11 System.IO.Compression
decompression, silesia.tar 1.11–1.21× 0.83–0.86×
decompression, small records 1.17–1.37× 0.91–0.94×
decompression, small records with a dictionary 1.36–1.52× 0.74–0.79×
compression, silesia.tar 0.96–1.07× 0.97–1.01×
compression, small records 0.87–1.00× 0.89–0.98×
compression, small records with a dictionary 0.87–1.26× 0.95–1.00×

The ranges span compression levels 1 to 19. The reference here is libzstd built from source with Apple clang 21 at -O3, which decompresses some 20 % faster than the libzstd NativeCompressions and .NET ship: against those, decompression is 1.3–1.5× as fast on arm64. On x64 it is 1.0–1.2× as fast as NativeCompressions' libzstd, as the repository's README shows.

The measurements use the data of zstd's own benchmarks, which bench/zstd-corpus.sh downloads:

  • silesia.tar is the Silesia corpus, which the benchmarks in zstd's README use, taken as one tar of 211,948,032 bytes compressed into one frame.
  • github is the 500 JSON records of GitHub users from zstd's regression tests, 407,963 bytes in all. Each record is its own frame, and all frames go through one compressor or decompressor.
  • github + dict is the same records with github.dict, the 110 KiB dictionary zstd's tests use with them.

Three implementations are compared:

  • libzstd 1.5.7 compiled at -O3 with Apple clang 21, the reference
  • ZstandardEncoder and ZstandardDecoder from .NET 11, which wrap the libzstd the runtime ships
  • ManagedCompressions.Zstd, compiled with Native AOT

The machine is an Apple M4 Pro (arm64) running macOS 26.7, on one thread. The three implementations run in one process and take turns, and each figure is the median of its rounds. MB/s means millions of bytes of uncompressed data per second. At every level the three write identical frames, which the benchmark checks, so one compression ratio holds for all of them. Decompression decodes libzstd's frames at each level.

Compression ratio

level 1 3 5 7 9 13 16 19
silesia.tar 2.896 3.205 3.378 3.518 3.588 3.674 3.834 4.011
github 2.866 2.992 3.019 3.019 3.019 3.070 3.063 3.070
github + dict 9.886 9.922 10.527 10.524 10.344 10.225 10.764 10.760

Compression, MB/s

corpus implementation 1 3 5 7 9 13 16 19
silesia.tar libzstd 1.5.7 (native) 786 460 239 153 118 23.6 10.6 5.12
.NET 11 (System.IO.Compression) 774 460 241 154 117 23.3 10.5 4.97
ManagedCompressions.Zstd (Native AOT) 773 467 237 149 113 25.2 10.9 5.09
github libzstd 1.5.7 (native) 323 306 212 203 155 39.9 15.4 12.6
.NET 11 (System.IO.Compression) 308 291 204 196 147 39.2 14.1 11.2
ManagedCompressions.Zstd (Native AOT) 300 268 185 178 144 39.9 14.3 11.4
github + dict libzstd 1.5.7 (native) 1188 1134 444 232 145 70.3 6.19 6.27
.NET 11 (System.IO.Compression) 1149 1096 439 230 143 70.6 5.92 5.98
ManagedCompressions.Zstd (Native AOT) 1133 1035 395 202 133 88.7 7.82 7.80

Compression, speedup over libzstd 1.5.7 (native)

corpus implementation 1 3 5 7 9 13 16 19
silesia.tar .NET 11 (System.IO.Compression) 0.99× 1.00× 1.01× 1.01× 1.00× 0.99× 0.99× 0.97×
ManagedCompressions.Zstd (Native AOT) 0.98× 1.01× 0.99× 0.98× 0.96× 1.07× 1.03× 0.99×
github .NET 11 (System.IO.Compression) 0.95× 0.95× 0.96× 0.96× 0.95× 0.98× 0.92× 0.89×
ManagedCompressions.Zstd (Native AOT) 0.93× 0.88× 0.87× 0.88× 0.93× 1.00× 0.93× 0.90×
github + dict .NET 11 (System.IO.Compression) 0.97× 0.97× 0.99× 0.99× 0.99× 1.00× 0.96× 0.95×
ManagedCompressions.Zstd (Native AOT) 0.95× 0.91× 0.89× 0.87× 0.92× 1.26× 1.26× 1.25×

Decompression, MB/s

corpus implementation 1 3 5 7 9 13 16 19
silesia.tar libzstd 1.5.7 (native) 2166 2022 2027 2190 2250 2332 2243 2100
.NET 11 (System.IO.Compression) 1873 1680 1675 1824 1874 1956 1871 1751
ManagedCompressions.Zstd (Native AOT) 2415 2423 2446 2637 2668 2745 2584 2538
github libzstd 1.5.7 (native) 774 537 542 542 541 539 547 549
.NET 11 (System.IO.Compression) 706 503 510 510 507 506 511 514
ManagedCompressions.Zstd (Native AOT) 1060 628 637 636 634 635 641 645
github + dict libzstd 1.5.7 (native) 3613 3743 3984 3207 3067 3258 3919 3908
.NET 11 (System.IO.Compression) 2781 2849 2961 2520 2431 2556 2946 2982
ManagedCompressions.Zstd (Native AOT) 4993 5361 5820 4439 4210 4425 5913 5930

Decompression, speedup over libzstd 1.5.7 (native)

corpus implementation 1 3 5 7 9 13 16 19
silesia.tar .NET 11 (System.IO.Compression) 0.86× 0.83× 0.83× 0.83× 0.83× 0.84× 0.83× 0.83×
ManagedCompressions.Zstd (Native AOT) 1.11× 1.20× 1.21× 1.20× 1.19× 1.18× 1.15× 1.21×
github .NET 11 (System.IO.Compression) 0.91× 0.94× 0.94× 0.94× 0.94× 0.94× 0.93× 0.94×
ManagedCompressions.Zstd (Native AOT) 1.37× 1.17× 1.17× 1.17× 1.17× 1.18× 1.17× 1.17×
github + dict .NET 11 (System.IO.Compression) 0.77× 0.76× 0.74× 0.79× 0.79× 0.78× 0.75× 0.76×
ManagedCompressions.Zstd (Native AOT) 1.38× 1.43× 1.46× 1.38× 1.37× 1.36× 1.51× 1.52×

To reproduce the measurements (macOS, .NET 11 SDK):

tools/native-ref/build.sh && bench/zstd-corpus.sh
dotnet publish bench/ManagedCompressions.Zstd.Perf -c Release -o artifacts/perf
artifacts/perf/ManagedCompressions.Zstd.Perf --corpus silesia,github,github-dict --results corpus.tsv --markdown corpus.md

On Windows x64, from Git Bash, the reference is built with a GNU-compatible clang such as llvm-mingw, because MSVC builds neither zstd's assembly Huffman decoder nor its BMI2 dispatch, which libzstd has wherever a GNU-compatible compiler builds it. --core N keeps the timed thread on one logical processor:

CC=<llvm-mingw>/bin/clang tools/native-ref/build.sh && bench/zstd-corpus.sh
dotnet publish bench/ManagedCompressions.Zstd.Perf -c Release -o artifacts/perf
artifacts/perf/ManagedCompressions.Zstd.Perf --core 2 --corpus silesia,github,github-dict --results corpus.tsv --markdown corpus.md

Testing

Correctness is checked against libzstd itself:

  • Differential tests against the libzstd of .NET's System.IO.Compression: ManagedCompressions.Zstd must give back the content libzstd decodes, and write libzstd's compressed frames byte for byte, with and without dictionaries.
  • zstd's golden files, for decompression, decompression errors and dictionaries.
  • A decodecorpus corpus: random valid frames from zstd's own generator. They exercise the modes an encoder rarely emits, such as RLE and repeated tables, treeless literals and direct Huffman weights.
  • Fuzzing, with buffers placed against guard pages.
  • Multithreaded compression, checked against libzstd built with threads. These tests are skipped until tools/native-ref/build.sh mt has built that library.

The golden files and the decodecorpus corpus are binary files that are generated rather than kept in the repository. tools/native-ref/testdata.sh writes them from zstd 1.5.7's sources into tests/ManagedCompressions.Zstd.Tests/testdata/. The tests that read them are skipped without them.

Building

tools/native-ref/testdata.sh
dotnet build -c Release
dotnet test

The SDK version is pinned in global.json. testdata.sh downloads the zstd 1.5.7 sources once, checks their SHA-256, builds zstd's decodecorpus with the system C compiler and writes the test data. It runs on macOS and Linux, and on Windows from Git Bash with a GNU-compatible clang as CC. The native reference that tools/native-ref/build.sh builds is only needed for the multithreaded compression tests and for the benchmarks: dylibs on macOS, DLLs on Windows (with the same CC), and shared objects elsewhere.

Repository layout

path contents
src/ManagedCompressions.Zstd the library
tests/ManagedCompressions.Zstd.Tests differential tests, golden files, unit tests, fuzzing
bench/ManagedCompressions.Zstd.Perf Native AOT benchmarks against libzstd and the platform, in turns (--corpus, --compress)
bench/ManagedCompressions.Benchmarks BenchmarkDotNet benchmarks against NativeCompressions and .NET
bench/zstd-corpus.sh downloads the benchmark data into tests/ManagedCompressions.Zstd.Tests/testdata/corpus
tools/native-ref the native reference, libzstd 1.5.7 built from source with its timing harness (build.sh), and the test data's generator (testdata.sh)
tests/ManagedCompressions.Zstd.Tests/testdata generated, not kept: zstd's golden files and the decodecorpus corpus (testdata.sh), and the benchmark data (zstd-corpus.sh)

License

ManagedCompressions.Zstd is licensed under the MIT License. Its compressor is derived from libzstd 1.5.7, and that part remains under zstd's BSD license, as does the test data copied from the zstd repository. See NOTICE, which the package carries.

Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (4)

Showing the top 4 NuGet packages that depend on ManagedCompressions.Zstd:

Package Downloads
Vorticity

Pure .NET implementation of the Vortex columnar file format, and Apache Parquet read and written by the same engine, with no third-party dependency.

Vorticity.RowEncoding

Byte-sortable row encoding for Vortex arrays: columns to row keys whose memcmp order is tuple order. Upstream marks the format experimental and may change its byte layout between releases.

Vorticity.Dataset

A versioned dataset over an object store: immutable data and commit objects, a prolly tree of leaves, and the store seam an S3 library implements. The format is experimental and this repository is the first to write it.

Vorticity.ObjectStorage

Object stores for Vorticity's files and datasets on Amazon S3, Cloudflare R2, OVHcloud, Scaleway, Azure Blob Storage and Google Cloud Storage, over the platform's HTTP client and nothing else.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 533 10/11/2026
0.1.0 86 10/11/2026