ACEAPEX has a fast profile (levels 11-18). Fixed 32-bit tokens, min match 16, min distance 64, independent 1 MiB blocks. The point of it is decode: no length branches, 64 literal and 96 match bytes copied per token.
Decode was fine. Encode was not. Level 11 did silesia at 25.8 MB/s. zxc -1 does the same file at ~1 GB/s. In lzbench that is the first thing people see.
So I took the obvious thing: lz4 fast. One slot per hash, step grows on misses. Result 65.16 % at 406 MB/s. Fast, but ratio fell apart. Below is where exactly it broke and what was built on top.
where the lz4 trick breaks on this format
- min distance 64. On runs and short periods the slot holds a position 1..63 bytes back. Format can not encode it. Match lost.
- min match 16. A probe lands in the middle of a match. lz4 starts the match at the probe, so the first part goes out as literals.
- insert only probed positions. Table is too sparse, long matches are missed between probes.
what was built
- 8-byte hash key. With min match 16 a 4-byte key only fills the table with junk candidates.
- Probe every A bytes, insert every B-th position, A and B coprime (A=3, B=2). Any match long enough is hit at some probe.
- Backward extension, 8 bytes per step, last <8 bytes in one compare. Recovers the real start of the match.
- Candidate closer than 64: lift distance to the first legal multiple,
d *= ceil(64/d), verify. Runs and short periods compress without any special mode. - 8..15 byte hit at the probe: extend backward, accept if it reaches 16.
- Inside a match insert every 8th position and
end-2. - Matches 65..128 bytes written as tokens of <=64 bytes (the decoder fast path copies 64).
Rule I got out of it, falsifiable by a grid (K in {4,5,6,8} x strides, it held):
P(find match of length L) ~ (L-K+1)/(A*B) * survival of the table entry
work per literal byte ~ c_probe/A + c_insert/B
You can not buy more speed with strides after that point. What is left is memory and archive assembly.
numbers
lzbench, Zen 5 (GCP C4D, EPYC 9B45), silesia.tar, one core, median of 5:
| codec | ratio | compress MB/s | decompress MB/s |
|---|---|---|---|
| aceapex_fast -1 (2.5.0) | 61.38 % | 575 | 15950 |
| zxc 0.15.0 -1 | 61.67 % | 852 | 11027 |
| memcpy | 28285 |
Decode +44.6 % over zxc -1 at a slightly better ratio. Compress is still behind, 575 vs 852. That is the next wall.
second thing that broke: lzbench builds without -march
First run on Zen 5 in lzbench gave only +24.7 % decode, our own harness +44 %. Reason: lzbench compiles codecs without -march, so the 32-byte copies became two SSE2 moves. Fix:
__attribute__((target_clones("avx2", "default")))
static uint32_t axf_dec_block(...);
x86-64 Linux, glibc, GCC 6+ / Clang 14+. Picked at load time. After it: +44.6 % in lzbench too. If your codec is fast only with -march=native, it is not fast in the benchmark table.
reproduce
aceapex 2.5.0 with aceapex_fast is in lzbench master since 29.10.2026 (PR #355):
git clone https://github.com/inikep/lzbench && cd lzbench && make -j
./lzbench -ezxc,1/aceapex_fast,1 -t8,8 -o1c4 silesia.tar
Codec source: https://github.com/yasha1971-coder/aceapex
Release 2.5.0, DOI 10.5281/zenodo.23748572. Logs of every number above are in the repo (results/).
Next: compress speed. Encoder already does 13.3 instructions per input byte (was 14.2), bytes identical. Will post the Zen 5 number when it is measured, not before.
Top comments (0)