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.

v0.15.0发布于 2026-10-13 12:26 UTC531star +4 / 7 天13fork同步于 2026-11-10 11:58 UTC

怎么上手

由 README 提炼,执行前请确认
brew install zxc
vcpkg install zxc
硬件要求按 README 说明

README 未说明显存或 GPU 要求;基准测试覆盖 Apple M2、Google Axion(ARM64)与 AMD EPYC Zen 5/Zen 3(x86_64),在 ARM64 上优势最明显。

支持 Apple Silicon
下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

面向一次写入多次读取场景的无损压缩 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 绑定

容易踩的坑

vcpkg、Conan Center 和 winget 的包由社区维护,可能落后于最新发布版本;上下文对象 zxc_cctx / zxc_dctx 是每线程的,不能跨线程共享;预编译二进制仅覆盖 Linux/Windows 的 x86_64 与 ARM64 以及 macOS ARM64。

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v0.15.0
  1. 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.zxc is 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 .zxd version 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 to libzxc.so.5: seekable block counts are 64-bit (#438), so programs linked against libzxc.so.4 must be rebuilt.

    Migrating v8 -> v9 is the same one-time transcode as before:

    zxc-old -dc old.zxc | zxc-new -z -c > new.zxc
    

    A 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_TABLE header 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 with ZXC_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_SIZE is replaced by ZXC_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.
    • .zxd dictionaries 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.zxc decodes 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_open spans 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 their FILE* 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
  2. 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_decompress ignored the dictionary size limit (#430). A dictionary above ZXC_DICT_SIZE_MAX was accepted, or rejected with a misleading ZXC_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). Cctx and Dctx now 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_range can verify per-block checksums through zxc_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 of ZXC_ERROR_CORRUPT_DATA.

    CLI

    • -l and -t read checksum status from the HAS_CHECKSUM header flag (#435). -j adds checksum_method; exit codes are unchanged. Thanks to @Vladexy88x for the fix.

    Performance

    • zxc_decompress_block writes straight to the destination (#418). Given the zxc_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 with ZXC_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.md gains 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's zxc-compress / zxc naming 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
  3. 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 stays libzxc.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.zxc
    

    Keep 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_tok and enc_off are 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:

    LevelApple M2Neoverse-V2EPYC Zen 5EPYC 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 -9 on both axes on every machine

  4. 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_off field 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_off GHI header field as 0 rather than 1, 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_off header field: 256 bytes when enc_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 claim enc_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_off is 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 = 0 in GHI headers, and FORMAT.md says 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 reset dict_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_IMPORT is opt-in from now on, so plain declarations link against both the DLL and the static library. CMake, M

  5. 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.

    LevelDecompression speedCompression 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() or FetchContent (#338). When built as a subproject: the CLI, tests, LTO and -march=native default to OFF; install, doc and formatting targets are disabled; compiler flags are inherited from the parent project instead of being overridden; and a zxc::zxc_lib ALIAS target is provided for consistent linking. A new ZXC_USE_SYSTEM_RAPIDHASH option controls whether the vendored or system rapidhash.h is 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

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    ZXC - Lossless Compression Built for Ultra-Fast Decode

    Build & Release Fuzzing Status Code Coverage OpenSSF Scorecard License

    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 M22.41x2.03x1.52x2.80x
    Axion (Neoverse-V2)1.93x1.54x1.29x2.17x
    EPYC 9B45 (Zen 5)2.19x1.65x1.24x2.49x
    EPYC 7B13 (Zen 3)1.82x1.35x1.15x2.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

    Decompression speed vs compressed size: ZXC is faster than the LZ4 family and zstd -1 at an equal or smaller size on Apple M2, Google Axion, AMD Zen 5 and AMD Zen 3

    All codecs

    Apple M2 (ARM64)

    CodecCompressionDecompressionRatio
    zxc 0.15.0 -1886 MB/s13513 MB/s61.76 %
    zxc 0.15.0 -2588 MB/s11320 MB/s53.86 %
    zxc 0.15.0 -3267 MB/s9707 MB/s46.65 %
    zxc 0.15.0 -4194 MB/s8790 MB/s43.43 %
    zxc 0.15.0 -5114 MB/s8118 MB/s40.83 %
    zxc 0.15.0 -614.5 MB/s6884 MB/s36.28 %
    zxc 0.15.0 -79.71 MB/s5054 MB/s33.09 %
    lz4 1.10.0 --fast -171347 MB/s5609 MB/s62.15 %
    lz4 1.10.0814 MB/s4774 MB/s47.60 %
    lz4hc 1.10.0 -948.4 MB/s4522 MB/s36.75 %
    lzav 5.17 -1686 MB/s3876 MB/s39.91 %
    snappy 1.3.1661 MB/s3258 MB/s47.85 %
    zstd 1.5.7 --fast --1723 MB/s2538 MB/s41.01 %
    zstd 1.5.7 -1645 MB/s1803 MB/s34.53 %
    zstd 1.5.7 -3375 MB/s1680 MB/s31.20 %
    zlib 1.3.2 -1135 MB/s378 MB/s36.45 %
    CodecCompressionDecompressionRatio
    zxc 0.15.0 -1885 MB/s9564 MB/s61.76 %
    zxc 0.15.0 -2593 MB/s7895 MB/s53.86 %
    zxc 0.15.0 -3256 MB/s6571 MB/s46.65 %
    zxc 0.15.0 -4186 MB/s6105 MB/s43.43 %
    zxc 0.15.0 -5109 MB/s5683 MB/s40.83 %
    zxc 0.15.0 -613.5 MB/s4950 MB/s36.28 %
    zxc 0.15.0 -78.82 MB/s3563 MB/s33.09 %
    lz4 1.10.0 --fast -171278 MB/s4954 MB/s62.15 %
    lz4 1.10.0730 MB/s4258 MB/s47.60 %
    lz4hc 1.10.0 -943.2 MB/s3850 MB/s36.75 %
    lzav 5.17 -1576 MB/s2949 MB/s39.91 %
    snappy 1.3.1566 MB/s2295 MB/s47.85 %
    zstd 1.5.7 --fast --1302 MB/s2291 MB/s41.01 %
    zstd 1.5.7 -1523 MB/s1642 MB/s34.53 %
    zstd 1.5.7 -3329 MB/s1525 MB/s31.20 %
    zlib 1.3.2 -1115 MB/s389 MB/s36.45 %
    CodecCompressionDecompressionRatio
    zxc 0.15.0 -1868 MB/s11609 MB/s61.76 %
    zxc 0.15.0 -2585 MB/s10332 MB/s53.86 %
    zxc 0.15.0 -3253 MB/s8335 MB/s46.65 %
    zxc 0.15.0 -4183 MB/s7508 MB/s43.43 %
    zxc 0.15.0 -5110 MB/s7014 MB/s40.83 %
    zxc 0.15.0 -615.3 MB/s6045 MB/s36.28 %
    zxc 0.15.0 -710.2 MB/s4724 MB/s33.09 %
    lz4 1.10.0 --fast -171284 MB/s5305 MB/s62.15 %
    lz4 1.10.0766 MB/s5052 MB/s47.60 %
    lz4hc 1.10.0 -945.4 MB/s4861 MB/s36.75 %
    lzav 5.17 -1699 MB/s3603 MB/s39.91 %
    snappy 1.3.1564 MB/s2420 MB/s47.89 %
    zstd 1.5.7 --fast --1664 MB/s2451 MB/s41.01 %
    zstd 1.5.7 -1606 MB/s1898 MB/s34.53 %
    zstd 1.5.7 -3369 MB/s1733 MB/s31.20 %
    zlib 1.3.2 -1134 MB/s392 MB/s36.45 %
    CodecCompressionDecompressionRatio
    zxc 0.15.0 -1724 MB/s8155 MB/s61.76 %
    zxc 0.15.0 -2481 MB/s6807 MB/s53.86 %
    zxc 0.15.0 -3214 MB/s5252 MB/s46.65 %
    zxc 0.15.0 -4157 MB/s4952 MB/s43.43 %
    zxc 0.15.0 -593.3 MB/s4779 MB/s40.83 %
    zxc 0.15.0 -611.9 MB/s4290 MB/s36.28 %
    zxc 0.15.0 -77.97 MB/s3188 MB/s33.09 %
    lz4 1.10.0 --fast -171108 MB/s4485 MB/s62.15 %
    lz4 1.10.0638 MB/s3883 MB/s47.60 %
    lz4hc 1.10.0 -937.1 MB/s3731 MB/s36.75 %
    lzav 5.17 -1459 MB/s2955 MB/s39.91 %
    snappy 1.3.1507 MB/s1837 MB/s47.89 %
    zstd 1.5.7 --fast --1486 MB/s1784 MB/s41.01 %
    zstd 1.5.7 -1444 MB/s1339 MB/s34.53 %
    zstd 1.5.7 -3232 MB/s1178 MB/s31.20 %
    zlib 1.3.2 -1106 MB/s358 MB/s36.45 %

    Throughput over a link

    Speed-up over an uncompressed transfer as a function of link bandwidth, for ZXC, LZ4 and Zstandard, on four CPUs

    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 -3 is 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 -1 while 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:

    ConanCenter Vcpkg Homebrew Debian 14 Ubuntu 26.10

    EcosystemInstall
    vcpkgvcpkg install zxc, or "dependencies": ["zxc"] in vcpkg.json
    Conanconan install -r conancenter --requires="zxc/[*]" --build=missing, or zxc/[*] under [requires] in conanfile.txt
    Homebrewbrew 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

    Crates.io PyPi npm

    Official bindings maintained in this repository:

    LanguagePackage ManagerInstall CommandDocumentationAuthor
    Rustcrates.iocargo add zxc-compressREADME@hellobertrand
    PythonPyPIpip install zxc-compressREADME@nuberchardzer1
    Node.jsnpmnpm install zxc-compressREADME@hellobertrand
    Gogo getgo get github.com/hellobertrand/zxc/bindings/goREADME@hellobertrand
    WASMnpmnpm install zxc-wasmREADME@hellobertrand

    Community-maintained bindings:

    LanguagePackage ManagerInstall CommandRepositoryAuthor
    Go (native decoder, no cgo)pkg.go.devgo get github.com/chmike/zxc-gohttps://github.com/chmike/zxc-go@chmike
    Nimnimblenimble install zxchttps://github.com/openpeeps/zxc-nim@georgelemon
    Free PascalBuild from sourceClone the repositoryhttps://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 Sizecctx memorydctx memoryRatio (level -3)Decompression gain vs 256 KB
    256 KB~1.04 MB~514 KB47.21%—
    512 KB (default)~1.79 MB~1.0 MB46.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/*.zxc streams with their expected output, and invalid/*.zxc streams a correct decoder must reject, at least one per row of the format's error table. Point your own decoder at them; locally, run the conformance CTest.
    • Encoder stability — tests/format/ pins the exact bytes the encoder emits for every block type and integrity field; vector-stability.yml fails 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:

    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.