A from-scratch C++ context-mixing compressor with binary arithmetic coding and adaptive logistic mixing across multiple order-N + sparse + word context models.
This is a simplified cousin of the paq8/cmix family of compressors that hold the Hutter Prize records on enwik9. The goal: a single .cpp file (~400 LoC) that beats xz -9e and brotli -q11 on Wikipedia text without LSTMs, transformers, or any of the heavy machinery cmix uses.
Tested on Wikipedia text from Matt Mahoney's large text compression benchmark, on a 2017 Dell Inspiron 7577 (Intel i7-7700HQ, 16 GB RAM, dual-channel DDR4).
| Compressor | size | bpb | speed |
|---|---|---|---|
gzip -9 |
371 KB | 2.831 | 0.4 s |
xz -9e |
303 KB | 2.310 | 0.4 s |
brotli -q 11 |
293 KB | 2.236 | ~3 s |
cmix_lite v1 |
287 KB | 2.189 | 4 s |
cmix_lite v2 |
273 KB | 2.080 | 5 s |
| Compressor | size | bpb | speed |
|---|---|---|---|
gzip -9 |
3.86 MB | 2.947 | 0.8 s |
xz -9e |
2.84 MB | 2.169 | 8 s |
brotli -q 11 |
2.83 MB | 2.157 | ~30 s |
cmix_lite v1 |
2.69 MB | 2.080 | 25 s |
cmix_lite v2 |
2.61 MB | 1.989 | 34 s |
| Compressor | size | bpb | speed |
|---|---|---|---|
gzip -9 |
36.5 MB | 2.918 | 7 s |
brotli -q 11 |
25.7 MB | 2.059 | ~3 min |
xz -9e |
24.8 MB | 1.987 | 105 s |
cmix_lite v1 |
25.2 MB | 2.016 | 212 s |
cmix_lite v2 |
24.0 MB | 1.922 | 372 s |
cmix v18 (reference) |
~14.7 MB | ~1.18 | days |
NNCP (Bellard) |
~13.1 MB | ~1.05 | hours |
fast-cmix (SOTA 2024) |
~10.7 MB | ~0.86 | days |
cmix_lite v2beats xz -9e and brotli -q11 on every test file.- On enwik6 we're 10 % smaller than xz.
- On enwik7 we're 8 % smaller than xz.
- On enwik8 we're 3 % smaller than xz (~800 KB difference on 100 MB).
- Hutter-prize SOTA (~0.86 bpb) is still ~2× more compression than us.
raw byte stream
│
▼
For each byte, encode as 8 bits MSB-first.
For each bit:
┌─────────────────────────────────────────────────┐
│ v2 model bank (10 inputs to mixer): │
│ 1. order-0 (no context) │
│ 2. order-1 (last byte) │
│ 3. order-2 (last 2 bytes) │
│ 4. order-3 (last 3 bytes, hashed) │
│ 5. order-5 (last 5 bytes, hashed) │
│ 6. order-8 (last 8 bytes, hashed) │
│ 7. sparse-2 (byte at offset -2) │
│ 8. sparse-3 (byte at offset -3) │
│ 9. sparse-5 (byte at offset -5) │
│ 10. word context (running hash of letters) │
│ Each gives P(bit=0) by counting bytes │
│ consistent with the prefix. │
└────────────────────┬────────────────────────────┘
│
┌───────────────────┼───────────────────┐
│ Logistic mixer (paq-style): │
│ s = Σ wᵢ · stretch(pᵢ) │
│ p = squash(s) │
│ Online gradient-descent on log-loss │
└───────────────────┬───────────────────┘
│
▼
Binary arithmetic coder
│
▼
compressed bitstream
After encoding each bit, the actual bit value is fed back to update both the context-model byte counters and the mixer weights. No offline training, no parameter tuning per file, no preprocessing -- single pass, fully online.
clang++ -O3 -std=c++17 -o cmix_lite cmix_lite.cpp # v1, simpler
clang++ -O3 -std=c++17 -o cmix_v2 cmix_v2.cpp # v2, with sparse + wordOr with MSVC:
cl /O2 /std:c++17 /EHsc cmix_lite.cpp
cl /O2 /std:c++17 /EHsc cmix_v2.cpp./cmix_v2 c input.txt input.cm # compress
./cmix_v2 d input.cm output.txt # decompress
| Model | bits per slot | slots | size |
|---|---|---|---|
| order-0 (direct) | 16 | 256 | 0.5 KB |
| order-1 (direct) | 16 | 65536 | 128 KB |
| order-2 (direct) | 16 | 16777216 | 32 MB |
| order-3 hash | 16 | 1048576 | 512 MB |
| order-5 hash | 16 | 1048576 | 512 MB |
| order-8 hash | 16 | 1048576 | 512 MB |
| sparse-2 / sparse-3 / sparse-5 | 16 | 262144 | 128 MB each |
| word hash | 16 | 1048576 | 512 MB |
| total v2 | ~2.5 GB |
Reduce the bits argument in the HashModel constructors if you need
to fit in less memory; expect a small bpb regression.
This implementation reaches ~2.0 bpb on Wikipedia text with very simple techniques. The Hutter Prize record (~0.86 bpb on enwik9) requires substantially more machinery:
- More context models. cmix uses ~300 distinct context functions including word+context joint contexts, indirect contexts (context = a hash of two contexts), match models, etc.
- Secondary symbol estimation (SSE). Take the mixer's predicted probability and look it up in a context-conditioned table to refine. Worth ~0.2 bpb.
- Multi-stage mixing. Several mixers in series, each refining the previous prediction in a different context space.
- Neural-network predictor. NNCP (2019) replaces the context models
with a small transformer trained online on the data being compressed.
fast-cmix(2024) combines an LSTM ensemble with classical contexts. - Pre-processors. WRT (Word-Replacing Transform) replaces common English words with short codes before compression, gaining ~5 % on text -- this is what cmix does.
A realistic next step from this baseline would be (effort vs. expected gain):
| step | est. bpb | est. effort |
|---|---|---|
| (done) v1: 6 byte-orders + logistic mix | 2.05 | -- |
| (done) v2: + sparse-2/3/5 + word context | 1.99 | hours |
| add SSE (one stage) | ~1.7 | hours |
| add cmix-style match model (find longest exact match) | ~1.55 | day |
| add small online-trained LSTM as another mixer input | ~1.15 | week |
| swap byte-count predictors for transformer (NNCP-style) | ~0.95 | weeks |
| full ensemble á la fast-cmix + WRT | ~0.86 | months, record territory |
cmix_lite.cpp— minimal C++ compressor (6 inputs)cmix_v2.cpp— enhanced C++ compressor (10 inputs, sparse + word)compressor.py— slow Python reference (~200× slower; useful for understanding/debugging)acoder.py— Python binary arithmetic codertest_acoder.py— round-trip tests for the Python coder
This was built in a single session as an educational baseline, not a serious record-breaking attempt. The Hutter Prize community has spent years refining cmix; competing with that requires their level of domain knowledge and weeks of CPU time per encode. The numbers above are real and reproducible, but understand the gap to SOTA: roughly 2× more compression (2.0 bpb → 0.86 bpb), 30+ years of incremental research, and large compute budgets.