vcpkg、Conan Center 和 winget 的包由社区维护,可能落后于最新发布版本;上下文对象 zxc_cctx / zxc_dctx 是每线程的,不能跨线程共享;预编译二进制仅覆盖 Linux/Windows 的 x86_64 与 ARM64 以及 macOS ARM64。
zxc
hellobertrandBSD-3-Clause活跃维护超快解码的无损压缩 C 库,解码速度比 LZ4 快 35%~105%,压缩率还更高,ARM64 上表现最佳,适合一次写入多次读取的场景。
Lossless compression C library built for ultra-fast decode. Faster than LZ4 (+35% to 105%+; best on ARM64), at a better ratio. Official Rust, Python, Node.js, Go and WASM bindings. Write-once/read-many, with optional seekable archives for O(1) random access.
brew install zxc
vcpkg install zxcREADME 未说明显存或 GPU 要求;基准测试覆盖 Apple M2、Google Axion(ARM64)与 AMD EPYC Zen 5/Zen 3(x86_64),在 ARM64 上优势最明显。
面向一次写入多次读取场景的无损压缩 C 库,解码速度远超 LZ4,压缩率还更好
超快解码的无损压缩 C 库,解码速度比 LZ4 快 35%~105%,压缩率还更高,ARM64 上表现最佳,适合一次写入多次读取的场景。
核心能力
- 解码速度达每核数 GB/s,-1 到 -6 档比同压缩率的 LZ4 快 1.1~2.4 倍
- -7 档压缩率优于 zstd -1,解码最高快 2.8 倍
- 提供可寻址归档,支持 O(1) 随机访问解压
- 支持原地解压与小数据字典压缩
- 官方提供 Rust、Python、Node.js、Go、WASM 绑定
容易踩的坑
- v0.15.0ZXC v0.15.0最新正式版2026-10-13 · 2026-10-13 12:26 UTC
Release Notes
v0.15.0 is an integrity release built on a format break, and a speed release on top of it.
Decompression gains up to 24 % at levels 3-5 and 9-14 % at level 7 over v0.14.x; compression gains up to 39 % at levels 3-7. Level 3 now decodes 2.03x faster than LZ4 on Apple M2 (1.75x in v0.14), at a smaller size.
Format v9 redoes every integrity field: checksums that leave no header bit unprotected, block checksums over the decoded bytes, an optional archive digest, a seek table that validates itself group by group. On top of it, ZXC archives become multi-frame:
cat a.zxc b.zxcis a valid archive, and the seekable reader spans every frame.⚠️ Breaking change (format and ABI). The container format is now v9, and dictionaries move to
.zxdversion 2. v0.15.0 cannot read v8 archives or version-1 dictionaries, and v0.14.x cannot read what v0.15.0 writes (ZXC_ERROR_BAD_VERSION). The soname moves tolibzxc.so.5: seekable block counts are 64-bit (#438), so programs linked againstlibzxc.so.4must be rebuilt.Migrating v8 -> v9 is the same one-time transcode as before:
zxc-old -dc old.zxc | zxc-new -z -c > new.zxcA dictionary is migrated with its archives: retrain it with the new build, or convert it byte for byte to keep its
dict_id. Full guide, including the CLI output changes and the new Windows archive layout: docs/MIGRATION.md.ZXC format v9
The compressed data is unchanged: same blocks, same parser, same entropy coders. The break is in what protects them and in how a reader finds its way. (#452)
- Multiplicative header checksums (#432). The XOR/shift construction left part of the compressed size unprotected; the same 2 bytes now cover every header bit.
- Block checksums cover the decoded bytes, seeded by position (#434). A block moved or duplicated inside an archive no longer verifies. The whole-archive hash gives way to an optional 8-byte archive digest folded from the block checksums.
- A seek table that validates itself (#436, #438). Groups of one 64-bit anchor and 64 block sizes, checked one group at a time and loaded lazily, so opening a large archive reads only what a range needs. Block counts are 64-bit.
HAS_SEEK_TABLEheader flag (#448). The table is announced instead of probed for. In v0.14, about one footer in 65 536 parsed as a seek-table header and failed an intact archive withZXC_ERROR_IO; that ambiguity is gone.- A variable-length footer (#489). Source size and compressed frame size are stored on the bytes they need, behind the optional digest, with a last byte giving their lengths: 3 to 17 bytes, plus 8 with the digest.
ZXC_FILE_FOOTER_SIZEis replaced byZXC_FILE_FOOTER_MIN_SIZE/ZXC_FILE_FOOTER_MAX_SIZE. - One valid form for the EOF and SEK headers. Reserved bytes must be zero there and decoders check them, so a lying flag reads as corrupt data rather than a truncated table.
.zxddictionaries version 2, signed with the new header checksum.
Multi-frame archives (#490)
A ZXC file is now one or more frames back to back. After each footer comes the end of the input, the next frame's magic, or an error.
- Concatenation just works.
cat a.zxc b.zxc > ab.zxcdecodes to both sources in order, through the buffer, context,FILE*and push-stream APIs, and the size queries add the frames up. Frames keep their own block size, checksum flag and dictionary. - Seekable across frames. When every frame carries a seek table,
zxc_seekable_openspans them all (up to 2^20 frames), with a dictionary per frame. - Frame info API (#489).
zxc_get_frame_info(),zxc_get_last_frame_info()and theirFILE*versions report a frame's sizes and flags; the compressed frame size in each footer lets a reader walk the frames back from the end. - Wrappers. Go, Rust and P
- v0.14.1ZXC v0.14.12026-09-19 · 2026-09-19 13:07 UTC
查看更新说明
Release Notes
v0.14.1 is a hardening release: every decode path got stricter, block decompression is 10-13 % faster, reusable contexts reach all five wrappers, and seekable archives can verify per-block checksums.
Drop-in for v0.14.0. Same format v8, byte-identical output at every level (verified on Silesia), soname still
libzxc.so.4. One symbol added,zxc_seekable_set_checksum; none removed or changed.Decoder hardening
- A forged block size passed off a truncated decode as success (#428). A payload size spanning the EOF marker made the frame walk run off the archive and return what it had, with checksums off (the default). A data block's payload can no longer exceed the header's block size, the walk must end on the EOF block, and a seek-table entry spans at most one block. The check sits in the block decoder shared by every reader. Both rules are now normative in
FORMAT.md. - Stack address stored in heap memory (#429). Multi-threaded seekable jobs held the caller's output pointer. This was harmless, since workers are joined before return, but a scanner can't prove that. Jobs now carry offsets from one base pointer on the caller's stack.
zxc_stream_decompressignored the dictionary size limit (#430). A dictionary aboveZXC_DICT_SIZE_MAXwas accepted, or rejected with a misleadingZXC_ERROR_DICT_MISMATCH. The check moved into the shared stream engine.- Contexts (#417, #422). Static contexts reject oversized blocks up front. An empty input through a context now yields the canonical 36-byte archive, like the one-shot API.
API
- Reusable contexts in every wrapper (#424, #414, #415).
CctxandDctxnow ship in Rust, Go, Python, Node.js and WASM. A dictionary given to the factory applies to every call that doesn't override it. The block API also takes a dictionary's shared literal Huffman table (#415), so small blocks no longer spend bytes describing their own. - Seekable checksum verification (#425).
zxc_seekable_decompress_rangecan verify per-block checksums throughzxc_seekable_set_checksum. It is off by default and exposed in all five wrappers. Also fixed: an overflowing offset plus length slipped past the bounds check, and an incomplete range returned a silent partial read instead ofZXC_ERROR_CORRUPT_DATA.
CLI
-land-tread checksum status from theHAS_CHECKSUMheader flag (#435).-jaddschecksum_method; exit codes are unchanged. Thanks to @Vladexy88x for the fix.
Performance
zxc_decompress_blockwrites straight to the destination (#418). Given thezxc_decompress_block_bound()capacity, it skips the bounce-buffer copy and runs 10-13 % faster, from 64 KB to 2 MB blocks (Apple M3, level 3, Silesia). An exact-size destination falls back to the bounce buffer instead of failing withZXC_ERROR_OVERFLOW.
Build & Packaging
- Trusted publishing for the release workflows (#421), replacing long-lived tokens.
- Rapidhash is resolved at configure time (#420). The vendored copy is used unless
-DZXC_USE_SYSTEM_RAPIDHASH=ON, and a missing copy fails at configure rather than mid-build. Thanks to @jeanga for the issue. - Wrapper packaging and release integrity tightened (#409). Publish jobs no longer install an unpinned global npm (#426, #427).
- One seeded checksum entry point (#423), tidier nudge tuning macros (#412), and SARIF uploads skipped on fork PRs.
Documentation
FORMAT.mdgains the two decoder rules and the dictionary ID derivation (#413): a 32-bit value that binds a dictionary to an archive without authenticating it. The Rust crate'szxc-compress/zxcnaming is clarified (#410), with README refinements (#408).Changelog
- api: Enforces strict block size and EOF validation (#428)
- api: Implements reusable compression contexts (#424)
- api: Enhances seekable API with checksum control and robust range validation (#425)
- api: Fix empty frame compressio
- A forged block size passed off a truncated decode as success (#428). A payload size spanning the EOF marker made the frame walk run off the archive and return what it had, with checksums off (the default). A data block's payload can no longer exceed the header's block size, the walk must end on the EOF block, and a seek-table entry spans at most one block. The check sits in the block decoder shared by every reader. Both rules are now normative in
- v0.14.0ZXC v0.14.02026-09-03 · 2026-09-03 09:04 UTC
查看更新说明
Release Notes
v0.14.0 is a decode-speed release built on a format break.
This release boosts decompression speed by up to 19% over v0.13.x, up to doubling our performance lead over LZ4 on ARM64 (+85% to +105%) and up to tripling it on x86_64 (+50% to +175%).
Three independent changes get there: blocks stop describing what the decoder can derive (format v8), short back-references are copied 32 bytes at a time instead of 16, and at levels 1-5 the encoder keeps its matches far enough back that the widest SIMD copy path stays on. The PivCo Huffman kernels also picked up AVX-512 VBMI, 256-bit AVX2 and NEON work.
Nothing in the public API moved: no symbol added, none removed, ABI unchanged (
libzxc.so.4). The break is in the container format.⚠️ Breaking change (format). The container format is now v8. v0.14.0 writes v8 archives that v0.13.x and earlier decoders reject, and it cannot read v7 or older: the header version byte is checked for exact equality (
ZXC_ERROR_BAD_VERSION). The ABI is unchanged (soname stayslibzxc.so.4), so no re-link is required — but upgrading the library alone makes newly written archives v8.Migrating v7 -> v8 is a one-time transcode — decompress with your old build, recompress with the new one:
zxc-old -dc old.zxc | zxc-new -z -c > new.zxcKeep a v7 build around until your data at rest is converted; if the old archive used a dictionary, pass it to the decompress side (
zxc-old -dc -D dict.zxd). Full guide (bulk migration, dictionaries, verification): docs/MIGRATION.md.ZXC format v8
Compression is untouched — same parser, same match finder, same entropy coders. What changed is how a block describes itself, and every change pulls the same way: stop writing down what the decoder can work out. (#374)
- Block sub-header: 16 -> 12 bytes. Four reserved bytes nobody used.
- Descriptors only when they carry information. v7 wrote a fixed 32-byte table of section sizes; most of them follow from the header. v8 keeps the two that don't: the literal section's compressed size when it is RLE- or entropy-coded, and the token section's at level 7. GLO now carries 0, 4 or 8 descriptor bytes, GHI none. The extras section is the leftover payload.
enc_litlen->enc_tok. The field always described the token section, never literal lengths.- 32 readable bytes must follow the literal section. Literals are copied with a wild copy that overshoots, and those bytes keep the last copy inside the payload. Real blocks clear the bar without trying; the encoder pads into the extras when a tiny block doesn't. Always relied on, never written down — now normative, and decoders must reject blocks that break it.
enc_lit,enc_tokandenc_offare closed value sets. Decoders reject unknown values instead of defaulting, which keeps the spare values free for a future version.
Per block this saves 25 to 34 bytes: invisible at the 512 KB default (levels 6 and 7 land 0.02 % smaller), worth about 2 % at the 4 KB blocks where dictionaries live.
Performance
Decompression on Silesia (202 MB), single-threaded, v0.13.3 -> v0.14.0 on the same reference hardware:
Level Apple M2 Neoverse-V2 EPYC Zen 5 EPYC Zen 3 -1 +6.5 % +2.8 % +4.5 % +3.4 % -2 +7.7 % +2.1 % +6.2 % +3.1 % -3 +19.0 % +16.6 % +10.9 % +13.4 % -4 +18.7 % +16.6 % +10.2 % +13.2 % -5 +18.0 % +16.8 % +9.8 % +13.0 % -6 +10.3 % +9.5 % +3.9 % +9.3 % -7 +9.2 % +6.2 % +1.7 % +6.2 % In absolute terms, level 3 decodes at 8.4 GB/s on Apple M2 and 6.7 GB/s on Zen 5; level 6 holds 6.7 GB/s on M2 at a 36.28 % ratio, ahead of
lz4hc -9on both axes on every machine - v0.13.3ZXC v0.13.32026-08-17 · 2026-08-17 19:19 UTC
查看更新说明
Release Notes
v0.13.3 is a hardening and packaging release.
The headline isn't really in the code: ZXC is now enrolled in Google OSS-Fuzz, which runs five harnesses around the clock. It has already turned up two decoder edge cases, both fixed here. Neither is reachable from an archive ZXC itself produced — you need a hand-crafted file to hit them — but a decompressor that can be walked out of its buffer is a decompressor worth fixing.
The rest is housekeeping. The GHI header's
enc_offfield is now formally unused, which is the ambiguity that let the first bug through; match-offset validation lost a counter it never needed; and static linking on Windows works again.Nothing changes in the format or the ABI. v0.13.3 reads and writes the same format v7 archives as the rest of the v0.13.x line, and the soname is still
libzxc.so.4.Drop-in for v0.13.x. No format break, no ABI change, nothing to re-link. Archives move both ways between v0.13.3 and any earlier v0.13.x release. One detail worth knowing: at levels 1 and 2 the encoder now writes the unused
enc_offGHI header field as0rather than1, so output at those levels is no longer byte-for-byte what v0.13.2 produced. It is still standard format v7, and every v0.13.x release reads it. Levels 3-7 are byte-identical.Reliability & Security Fixes
Out-of-bounds read on forged GHI offsets (decoder)
The GHI safe loop took its offset-validation threshold from the
enc_offheader field: 256 bytes whenenc_off == 1. That field never described GHI offsets, which live inline in the sequence word and are always 16 bits wide. So a crafted archive could claimenc_off = 1, carry a much larger offset, and once 256 bytes had been written the loop simply stopped checking — and the copy read from before the start of the output buffer. The threshold is now the 16-bit bound, unconditionally. Found by OSS-Fuzz (544800535). (#367)Output-buffer overflow on escaped literal lengths (decoder)
When a sequence escapes its literal length into a varint, the GLO and GHI decode macros used to check that the literals fit in the remaining output space — and nothing else. They ignored the match length that follows, and the output still owed by the rest of the batch. A crafted archive could clear that check and then write past the end of the destination buffer. Both macros now weigh literals, match length and the batch remainder together, and the trailing-literal copy has an explicit guard in front of it. Found by OSS-Fuzz (546426939). (#369)
enc_offis unused in GHI blocks and must be ignored#367 was really a spec problem, so the field is now nailed down on both sides. The compressor always writes
enc_off = 0in GHI headers, andFORMAT.mdsays decoders must ignore it. GHI has no offset stream and its sequence words always carry 16-bit offsets, so there is nothing for the field to bound. (#368)Stale dictionary size on a reused compression context
zxc_compress_cctx()never resetdict_size. Reuse a context after a dictionary compression and the old dictionary size came along for the ride into the next, plain one. It is reset on every call now. (#351)Performance
Leaner match-offset validation
Match-offset validation used to lean on a running count of bytes written, updated on every single sequence. That counter is gone. The decoder now compares the match source directly to the start of the output buffer, or of the dictionary prefix when one is attached. Same guarantee, less bookkeeping in the hot loop. (#368)
Build, Packaging & Portability
Windows static-library linking
__declspec(dllimport)and static libraries don't get along on Windows: the link fails outright.ZXC_DLL_IMPORTis opt-in from now on, so plain declarations link against both the DLL and the static library. CMake, M - v0.13.2ZXC v0.13.22026-07-30 · 2026-07-30 07:01 UTC
查看更新说明
Release Notes
Release v0.13.2 is a performance and hardening patch release. The headline change: levels 6 and 7 now decompress +2–3 % and +10–11 % faster respectively, thanks to a decode-oriented nudge of the Huffman code lengths, at a near-zero ratio cost: compressed output grows by just 0.05 % at level 6 and 0.27 % at level 7. It also fixes two decoder edge cases (an out-of-bounds read on short RAW literal sections, and an in-place bound that could under-reserve the trailing framing), makes ZXC embeddable as a CMake subproject, and shrinks the shared library by removing duplicate SIMD variants. There are no changes to the container format or the ABI: v0.13.2 reads and writes the same format v7 archives as v0.13.0 and v0.13.1, and the shared-library soname stays
libzxc.so.4.Drop-in for v0.13.0 / v0.13.1. No format break, no ABI change, no re-link required. Every archive written by any v0.13.x release reads back identically in v0.13.2 and vice versa. One nuance: at levels 6 and 7 the encoder now emits slightly flatter Huffman trees, so archives written at those levels are no longer byte-for-byte identical to v0.13.1's output but they remain standard format v7, readable by every v0.13.x release. Levels 1-5 produce byte-identical output.
Performance
Faster decompression at levels 6 and 7
The encoder now applies a joint flat/length nudge to Huffman code lengths (#336): trees are flattened wherever it costs almost nothing in compressed size, so the PivCo decoder spends fewer cycles per symbol. At level 7, a dynamic-programming tier picks the best nudge per block; level 6 gets the same nudge in its lighter form (#343). Idea by @dougallj.
Level Decompression speed Compression size 6 +2% to +3% +0.05% 7 +10% to +11% +0.27% Levels 1-5 are unaffected.
Reliability Fixes
Out-of-bounds read on short RAW literal sections (decoder)
RAW literal sections decoded directly from the input buffer could read a few bytes past the end of the compressed input when the section tail was shorter than the decoder's padding requirement and no data followed it. Such sections are now staged through a padded internal buffer, and the conformance tests cover these layouts. (#341)
In-place bound now reserves the trailing framing
zxc_inplace_margin()ignored what the encoder writes after the last data block — the EOF block header and, for seekable archives, the seek table — so the returned bound could fall below the compressed size for seekable archives of incompressible data with small blocks. The worst-case seek table is now reserved unconditionally (4 bytes per block, at most 0.1 % of the payload at the minimum block size, negligible at the default). (#337)Embedding & Portability
Vendor ZXC as a CMake subproject
ZXC can now be embedded directly into another CMake build via
add_subdirectory()orFetchContent(#338). When built as a subproject: the CLI, tests, LTO and-march=nativedefault to OFF; install, doc and formatting targets are disabled; compiler flags are inherited from the parent project instead of being overridden; and azxc::zxc_libALIAS target is provided for consistent linking. A newZXC_USE_SYSTEM_RAPIDHASHoption controls whether the vendored or systemrapidhash.his used. The README documents the vendoring workflow.No more compiler-rt dependency for x86 CPU detection
CPUID and XGETBV are now issued through direct intrinsics and inline assembly instead of compiler-specific builtins, removing the implicit compiler-rt dependency and making the runtime dispatch robust across more toolchains. (#339)
Build, Binary Size & CI
- Duplicate SIMD variants removed: the SSE2 and NEON function-multi-versioning variants duplicated the baseline code paths and are gone, shrinking the shared library by roughly a third
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
ZXC - Lossless Compression Built for Ultra-Fast Decode
ZXC is a fast lossless compression algorithm, targeting write-once, read-many workloads: data compressed once at build time, then decompressed on every device that reads it. It features an extremely fast decoder, with speeds of multiple GB/s per core: levels -1 to -6 decode 1.1x to 2.4x faster than LZ4 (lz4 --fast, lz4 or lz4hc, whichever matches the ratio) at an equal or better compression ratio. Level -7, the densest, compresses better than zstd -1 while decoding up to 2.8x faster.
ZXC also provides seekable archives for O(1) random access, in-place decompression, and dictionary compression for small data.
The ZXC format is fully specified in FORMAT.md and guarded by public conformance vectors. This repository is the reference implementation, provided as an open-source BSD 3-Clause licensed C library and a command line utility producing and decoding .zxc files, with official bindings for Rust, Python, Node.js, Go and WASM. The design is described in the whitepaper.
Benchmarks
Decompression speedup against the closest competitor at each ratio tier — Silesia corpus (202 MB), single thread, reproducible with lzbench and TurboBench:
| Machine | -1 vs lz4 --fast | -3 vs lz4 | -6 vs lz4hc -9 | -7 vs zstd -1 |
|---|---|---|---|---|
| Apple M2 | 2.41x | 2.03x | 1.52x | 2.80x |
| Axion (Neoverse-V2) | 1.93x | 1.54x | 1.29x | 2.17x |
| EPYC 9B45 (Zen 5) | 2.19x | 1.65x | 1.24x | 2.49x |
| EPYC 7B13 (Zen 3) | 1.82x | 1.35x | 1.15x | 2.38x |
ZXC also compresses smaller in each pairing: 61.76 % vs 62.15 %, 46.65 % vs 47.60 %, 36.28 % vs 36.75 % and 33.09 % vs 34.53 % of the original size.
Measured with lzbench 2.4.1 (by @inikep), MOREFLAGS="-march=native":
Apple M2 (Clang 21, macOS 26), Google Axion / Neoverse-V2 (GCC 14, GCP C4A), AMD EPYC 9B45 / Zen 5
(GCP C4D) and AMD EPYC 7B13 / Zen 3 (GCP C2D), x86 with SMT disabled. Re-run on every commit
(latest logs). TurboBench
carries ZXC alongside 70+ other codecs. Cycles per byte and memory:
whitepaper.
Every level, on every CPU
All codecs
Apple M2 (ARM64)
| Codec | Compression | Decompression | Ratio |
|---|---|---|---|
| zxc 0.15.0 -1 | 886 MB/s | 13513 MB/s | 61.76 % |
| zxc 0.15.0 -2 | 588 MB/s | 11320 MB/s | 53.86 % |
| zxc 0.15.0 -3 | 267 MB/s | 9707 MB/s | 46.65 % |
| zxc 0.15.0 -4 | 194 MB/s | 8790 MB/s | 43.43 % |
| zxc 0.15.0 -5 | 114 MB/s | 8118 MB/s | 40.83 % |
| zxc 0.15.0 -6 | 14.5 MB/s | 6884 MB/s | 36.28 % |
| zxc 0.15.0 -7 | 9.71 MB/s | 5054 MB/s | 33.09 % |
| lz4 1.10.0 --fast -17 | 1347 MB/s | 5609 MB/s | 62.15 % |
| lz4 1.10.0 | 814 MB/s | 4774 MB/s | 47.60 % |
| lz4hc 1.10.0 -9 | 48.4 MB/s | 4522 MB/s | 36.75 % |
| lzav 5.17 -1 | 686 MB/s | 3876 MB/s | 39.91 % |
| snappy 1.3.1 | 661 MB/s | 3258 MB/s | 47.85 % |
| zstd 1.5.7 --fast --1 | 723 MB/s | 2538 MB/s | 41.01 % |
| zstd 1.5.7 -1 | 645 MB/s | 1803 MB/s | 34.53 % |
| zstd 1.5.7 -3 | 375 MB/s | 1680 MB/s | 31.20 % |
| zlib 1.3.2 -1 | 135 MB/s | 378 MB/s | 36.45 % |
| Codec | Compression | Decompression | Ratio |
|---|---|---|---|
| zxc 0.15.0 -1 | 885 MB/s | 9564 MB/s | 61.76 % |
| zxc 0.15.0 -2 | 593 MB/s | 7895 MB/s | 53.86 % |
| zxc 0.15.0 -3 | 256 MB/s | 6571 MB/s | 46.65 % |
| zxc 0.15.0 -4 | 186 MB/s | 6105 MB/s | 43.43 % |
| zxc 0.15.0 -5 | 109 MB/s | 5683 MB/s | 40.83 % |
| zxc 0.15.0 -6 | 13.5 MB/s | 4950 MB/s | 36.28 % |
| zxc 0.15.0 -7 | 8.82 MB/s | 3563 MB/s | 33.09 % |
| lz4 1.10.0 --fast -17 | 1278 MB/s | 4954 MB/s | 62.15 % |
| lz4 1.10.0 | 730 MB/s | 4258 MB/s | 47.60 % |
| lz4hc 1.10.0 -9 | 43.2 MB/s | 3850 MB/s | 36.75 % |
| lzav 5.17 -1 | 576 MB/s | 2949 MB/s | 39.91 % |
| snappy 1.3.1 | 566 MB/s | 2295 MB/s | 47.85 % |
| zstd 1.5.7 --fast --1 | 302 MB/s | 2291 MB/s | 41.01 % |
| zstd 1.5.7 -1 | 523 MB/s | 1642 MB/s | 34.53 % |
| zstd 1.5.7 -3 | 329 MB/s | 1525 MB/s | 31.20 % |
| zlib 1.3.2 -1 | 115 MB/s | 389 MB/s | 36.45 % |
| Codec | Compression | Decompression | Ratio |
|---|---|---|---|
| zxc 0.15.0 -1 | 868 MB/s | 11609 MB/s | 61.76 % |
| zxc 0.15.0 -2 | 585 MB/s | 10332 MB/s | 53.86 % |
| zxc 0.15.0 -3 | 253 MB/s | 8335 MB/s | 46.65 % |
| zxc 0.15.0 -4 | 183 MB/s | 7508 MB/s | 43.43 % |
| zxc 0.15.0 -5 | 110 MB/s | 7014 MB/s | 40.83 % |
| zxc 0.15.0 -6 | 15.3 MB/s | 6045 MB/s | 36.28 % |
| zxc 0.15.0 -7 | 10.2 MB/s | 4724 MB/s | 33.09 % |
| lz4 1.10.0 --fast -17 | 1284 MB/s | 5305 MB/s | 62.15 % |
| lz4 1.10.0 | 766 MB/s | 5052 MB/s | 47.60 % |
| lz4hc 1.10.0 -9 | 45.4 MB/s | 4861 MB/s | 36.75 % |
| lzav 5.17 -1 | 699 MB/s | 3603 MB/s | 39.91 % |
| snappy 1.3.1 | 564 MB/s | 2420 MB/s | 47.89 % |
| zstd 1.5.7 --fast --1 | 664 MB/s | 2451 MB/s | 41.01 % |
| zstd 1.5.7 -1 | 606 MB/s | 1898 MB/s | 34.53 % |
| zstd 1.5.7 -3 | 369 MB/s | 1733 MB/s | 31.20 % |
| zlib 1.3.2 -1 | 134 MB/s | 392 MB/s | 36.45 % |
| Codec | Compression | Decompression | Ratio |
|---|---|---|---|
| zxc 0.15.0 -1 | 724 MB/s | 8155 MB/s | 61.76 % |
| zxc 0.15.0 -2 | 481 MB/s | 6807 MB/s | 53.86 % |
| zxc 0.15.0 -3 | 214 MB/s | 5252 MB/s | 46.65 % |
| zxc 0.15.0 -4 | 157 MB/s | 4952 MB/s | 43.43 % |
| zxc 0.15.0 -5 | 93.3 MB/s | 4779 MB/s | 40.83 % |
| zxc 0.15.0 -6 | 11.9 MB/s | 4290 MB/s | 36.28 % |
| zxc 0.15.0 -7 | 7.97 MB/s | 3188 MB/s | 33.09 % |
| lz4 1.10.0 --fast -17 | 1108 MB/s | 4485 MB/s | 62.15 % |
| lz4 1.10.0 | 638 MB/s | 3883 MB/s | 47.60 % |
| lz4hc 1.10.0 -9 | 37.1 MB/s | 3731 MB/s | 36.75 % |
| lzav 5.17 -1 | 459 MB/s | 2955 MB/s | 39.91 % |
| snappy 1.3.1 | 507 MB/s | 1837 MB/s | 47.89 % |
| zstd 1.5.7 --fast --1 | 486 MB/s | 1784 MB/s | 41.01 % |
| zstd 1.5.7 -1 | 444 MB/s | 1339 MB/s | 34.53 % |
| zstd 1.5.7 -3 | 232 MB/s | 1178 MB/s | 31.20 % |
| zlib 1.3.2 -1 | 106 MB/s | 358 MB/s | 36.45 % |
Throughput over a link
Reading the chart
Data compressed once is read many times over a link: disk, network, another core. With transfer and decoding overlapped, a reader gets
min(decode speed, bandwidth ÷ ratio)of original data per second. The chart divides that by the bandwidth, so 1x is sending the data uncompressed, and draws each codec at its best level for the link. Compression time is left out, as it is paid once.
- Below 0.4–0.6 GB/s the ratio decides, and
zstd -3is ahead.- From there on, a ZXC level delivers the most data: -7 up to 1.1–1.7 GB/s, then the faster levels in turn. No LZ4 or LZ4HC level is ever above the ZXC curve.
- Compression stops paying once one decoding thread is slower than the link: past 8.2–13.5 GB/s for
zxc -1, 4.5–5.6 GB/s for LZ4.
Features
- Write once, read many. The encoder does the heavy lifting, so every device that reads the data decodes faster: content delivery, game assets, app bundles, firmware. Gains are largest on modern CPUs (benchmarks).
- O(1) random access. An optional seek table (
-S) decompresses any block without reading the rest. - Decodes in place. One buffer instead of two, zero allocations with a static context: made for firmware, FOTA and bootloaders (details).
- Small payloads too. A trained dictionary recovers ratio on 4–128 KB blocks (details).
- Runs everywhere. x86_64, ARM64, ARMv7, ARMv6, RISC-V, POWER, s390x, i386, with hand-tuned SIMD (AVX2/AVX-512/NEON).
- Production-grade. Continuously fuzzed by OSS-Fuzz, ASan/UBSan/Valgrind-clean, a specified wire format with conformance vectors, signed releases, BSD-3-Clause.
Used in ClickHouse (experimental column codec) · Packaged in Debian 14, Ubuntu 26.10, Homebrew, vcpkg, Conan, Winget · Benchmarked in lzbench & TurboBench
Compression Levels
- Levels 1–2 (Fast): real-time assets: games, UI.
- Levels 3–4 (Balanced, 3 is the default): a better ratio than LZ4 at efficient compression speed.
- Level 5 (Compact): embedded and firmware: a better ratio than LZ4, much faster decode than Zstandard.
- Level 6 (Density): a better ratio and faster decode than LZ4HC on every measured platform; for archival, where compression time is amortized over many reads.
- Level 7 (Ultra): maximum density, past
zstd -1while decoding more than twice as fast. Compression is the slowest tier.
Usage
1. CLI
# Compress. -z is implied, and the output name defaults to <input>.zxc
zxc assets.tar # level 3 (default) -> assets.tar.zxc
zxc -z -5 assets.tar assets.tar.zxc # level 5
zxc -z -S assets.tar assets.tar.zxc # seekable: O(1) random-access decompression
# Decompress. "unzxc" (POSIX installs) is an alias for "zxc -d"
zxc -d assets.tar.zxc assets.tar
unzxc assets.tar.zxc assets.tar
# Decompress to stdout. "zxccat" (POSIX installs) runs "zxc -dcf" on each file
zxccat logs-1.zxc logs-2.zxc | grep ERROR
# Read a compressed file page by page in less (POSIX installs)
zxcless app.log.zxc
# Search compressed files with grep's options (POSIX installs)
zxcgrep -n ERROR logs-*.zxc
# Benchmark mode (testing speed on your machine)
zxc -b assets.tar
Every option is in zxc --help and the man page.
Using with tar
ZXC works as a drop-in external compressor for tar (reads stdin, writes stdout, returns 0 on success):
# GNU tar (Linux)
tar -I 'zxc -5' -cf archive.tar.zxc data/
tar -I 'zxc -d' -xf archive.tar.zxc
# bsdtar (macOS)
tar --use-compress-program='zxc -5' -cf archive.tar.zxc data/
tar --use-compress-program='zxc -d' -xf archive.tar.zxc
# Pipes (universal)
tar cf - data/ | zxc > archive.tar.zxc
zxc -d < archive.tar.zxc | tar xf -
2. API
The one-shot calls are stateless and thread-safe: buffers are caller-allocated with explicit
bounds, and the simple forms take a level and nothing else. Every other setting is a parameter
on a reusable context, set by identifier, so call sites stay readable and forward-compatible.
Contexts are per-thread: never share a zxc_cctx / zxc_dctx between threads.
#include "zxc.h"
// One-shot: level 3, 512 KB blocks, no checksum, no dictionary
size_t bound = zxc_compress_bound(src_size);
int64_t compressed_size = zxc_compress(src, src_size, dst, bound, ZXC_LEVEL_DEFAULT);
int64_t decompressed_size = zxc_decompress(dst, (size_t)compressed_size, out, out_capacity);
// Anything else: configure a context (settings persist across calls)
zxc_cctx* cctx = zxc_create_cctx();
zxc_cctx_set_param(cctx, ZXC_C_LEVEL, ZXC_LEVEL_COMPACT);
zxc_cctx_set_param(cctx, ZXC_C_CHECKSUM, 1);
compressed_size = zxc_compress_cctx(cctx, src, src_size, dst, bound);
zxc_free_cctx(cctx);
zxc_dctx* dctx = zxc_create_dctx();
zxc_dctx_set_param(dctx, ZXC_D_CHECKSUM, 1); // verify what the archive carries
decompressed_size = zxc_decompress_dctx(dctx, dst, (size_t)compressed_size, out, out_capacity);
zxc_free_dctx(dctx);
The same contexts configure zxc_stream_compress() / zxc_stream_decompress() (multi-threaded
file streaming, one worker per core by default; the context is read, its buffers unused), skip
per-call allocation in tight loops, carry a dictionary (zxc_cctx_set_dict()), and with
ZXC_C_SEEKABLE set to 1 append a seek table for O(1) random access. A setter refuses an
out-of-range value rather than clamp it, and 0 restores a parameter's default.
👉 See complete examples and advanced usage — stream API, reusable contexts, seekable readers, dictionaries and numeric pre-filters, as full compilable programs.
Installation
ZXC is packaged in the major ecosystems:
| Ecosystem | Install |
|---|---|
| vcpkg | vcpkg install zxc, or "dependencies": ["zxc"] in vcpkg.json |
| Conan | conan install -r conancenter --requires="zxc/[*]" --build=missing, or zxc/[*] under [requires] in conanfile.txt |
| Homebrew | brew install zxc |
| winget (Windows 10 1709+) | winget install hellobertrand.zxc |
The vcpkg, Conan Center and winget recipes are community-maintained and can lag a release behind; if one does, open an issue on that registry's index repository. Rust, Python, Node.js, Go and WASM packages: Language Bindings.
From a release archive
Prebuilt binaries for Linux and Windows (x86_64 and ARM64) and macOS (ARM64) are on the Releases page, with a signed checksum manifest and build attestations: verification steps.
In your project
find_package(zxc REQUIRED) # find_package(zxc CONFIG REQUIRED) via vcpkg or Conan
target_link_libraries(myapp PRIVATE zxc::zxc_lib)
cc myapp.c $(pkg-config --cflags --libs libzxc) -o myapp
Vendoring zxc instead — CMake FetchContent or add_subdirectory(), a Meson subproject or WrapDB —
and building from source, with the full option table and the PGO workflow:
docs/INSTALL.md.
Language Bindings
Official bindings maintained in this repository:
| Language | Package Manager | Install Command | Documentation | Author |
|---|---|---|---|---|
| Rust | crates.io | cargo add zxc-compress | README | @hellobertrand |
| Python | PyPI | pip install zxc-compress | README | @nuberchardzer1 |
| Node.js | npm | npm install zxc-compress | README | @hellobertrand |
| Go | go get | go get github.com/hellobertrand/zxc/bindings/go | README | @hellobertrand |
| WASM | npm | npm install zxc-wasm | README | @hellobertrand |
Community-maintained bindings:
| Language | Package Manager | Install Command | Repository | Author |
|---|---|---|---|---|
| Go (native decoder, no cgo) | pkg.go.dev | go get github.com/chmike/zxc-go | https://github.com/chmike/zxc-go | @chmike |
| Nim | nimble | nimble install zxc | https://github.com/openpeeps/zxc-nim | @georgelemon |
| Free Pascal | Build from source | Clone the repository | https://github.com/Xelitan/Free-Pascal-port-of-ZXC-compressor-decompressor | @Xelitan |
In-Place Decompression
A normal decode needs two buffers, so peak memory is compressed + decompressed. ZXC can decode inside a single buffer: place the archive at its end, and the decoder writes the output from its start.
buffer: [ output → → → | archive → → → ]
^ write cursor ^ read cursor
Both cursors move right and a ZXC block never expands, so with a small margin the write cursor provably never catches the read cursor: peak memory drops to roughly decompressed alone. This is the shape of firmware images, FOTA payloads, bootloaders, game assets: anything read-only that already sits in RAM.
size_t need = zxc_decompress_inplace_bound(archive, archive_size); // reads header + footer only
uint8_t* buf = malloc(need);
memcpy(buf + (need - archive_size), archive, archive_size); // archive at the end
int64_t n = zxc_decompress_inplace(buf, need, archive_size); // decode from buf[0]
// buf[0 .. n) now holds the decompressed data
The margin is about one block, so small payloads want small blocks (-B). An undersized buffer, or an archive whose output would reach input not yet read, is rejected with an error, never silent corruption; both entry points are fuzzed. For kernel code and heapless targets, zxc_decompress_inplace_dctx with a static context touches no allocator at all. Margin and context sizes per block size, and the static-context API: API.md.
Dictionary Compression
For data compressed in small blocks (4 KB–128 KB), a pre-trained dictionary substantially
improves the ratio. It prefills the LZ77 window at the start of each block, so the smaller the
block, the more it gains: a single small payload, or a large corpus split into small blocks for
random access. A .zxd also carries a shared literal Huffman table (levels 6–7), which spares small
blocks their own 128-byte table header.
Typical use cases: JSON API responses, small game assets, structured logs, key-value store records, RPC messages, seekable archives with small blocks.
# Train from a corpus of similar files. Without -o: ./dictionary_<dict_id>.zxd
zxc --train samples/*.json
zxc --train -o corpus.zxd samples/*.json # -o also accepts a directory
# The same dictionary is required to decompress: pass it with -D, there is no auto-lookup
zxc -z -D corpus.zxd input.json
zxc -d -D corpus.zxd input.json.zxc
The archive records only the dictionary's dict_id: decompressing without it returns
ZXC_ERROR_DICT_REQUIRED, with the wrong one ZXC_ERROR_DICT_MISMATCH. From C:
EXAMPLES.md and
API.md §11b. Wire format:
FORMAT.md §12.
Block Size Tuning
Blocks are powers of two from 4 KB to 2 MB. The default, 512 KB, favours ratio and decode speed: each block starts with a cold match history, so larger blocks mean fewer cold starts. For streaming, embedded or memory-constrained use, 256 KB blocks halve the decompression context and cut the compression context by ~40 %, at a small cost in ratio and decode speed.
| Block Size | cctx memory | dctx memory | Ratio (level -3) | Decompression gain vs 256 KB |
|---|---|---|---|---|
| 256 KB | ~1.04 MB | ~514 KB | 47.21% | — |
| 512 KB (default) | ~1.79 MB | ~1.0 MB | 46.65% (−0.56 pp) | +1% to +8% depending on CPU |
# CLI — fall back to 256 KB blocks (e.g. embedded / streaming)
zxc -B 256K -5 input_file output_file
# API
zxc_cctx* cctx = zxc_create_cctx();
zxc_cctx_set_param(cctx, ZXC_C_LEVEL, ZXC_LEVEL_COMPACT);
zxc_cctx_set_param(cctx, ZXC_C_BLOCK_SIZE, 256 * 1024); // power of two, 4 KB..2 MB
Format & Conformance
The wire format is fully specified in docs/FORMAT.md (format version 9), so any third party can build an independent, interoperable decoder. Each format break is clean: v9 tools reject v8 archives, and docs/MIGRATION.md lists what changed and how to convert.
Two byte-frozen suites guard the format:
- Decoder conformance —
tests/conformance/ships public vectors per format version:valid/*.zxcstreams with their expected output, andinvalid/*.zxcstreams a correct decoder must reject, at least one per row of the format's error table. Point your own decoder at them; locally, run theconformanceCTest. - Encoder stability —
tests/format/pins the exact bytes the encoder emits for every block type and integrity field;vector-stability.ymlfails CI on any single-byte drift.
Safety & Quality
- Continuous fuzzing: Google OSS-Fuzz runs eight harnesses (roundtrip, decompress, block, in-place, push streaming, multi-threaded FILE* streaming, seekable, dictionary) around the clock, covering over 80 % of the library's lines (coverage history). The same harnesses run under ClusterFuzzLite (ASan + UBSan) on every pull request touching the library.
- Dynamic analysis: the CTest suite runs under ASan, UBSan and Valgrind in CI, with coverage tracked on Codecov.
- Static analysis: Cppcheck, Clang Static Analyzer, CodeQL and Snyk.
- Safe API: every operation takes an explicit buffer capacity.
License & Credits
ZXC Copyright © Bertrand Lebonnois and contributors. Licensed under the BSD 3-Clause License. See LICENSE for details; bundled third-party components are listed in THIRD_PARTY_NOTICES.
Third-Party Components:
- rapidhash by Nicolas De Carli (MIT) - Used for high-speed, platform-independent checksums.
Acknowledgements:
- PivCo-Huffman (PIVoted COding) by Marcin Żukowski - the level-ordered layout and merge-based decode of ZXC's Huffman sections follow its design; implemented independently here. Special thanks to Dougall Johnson, whose idea of jointly nudging flat and length codes is behind the level 6 / 7 decode speedups.