Skip to content

Commit c32ae6b

Browse files
paigejulianneclaude
andcommitted
Add multi-threaded compression via SQ01 multi-block format
The model pipeline is bit-serial within a stream, so parallelism comes from chunking: the SQ01 container wraps independent SQ02 chunk streams that compress and decompress concurrently, one model per worker. - New API: squish_compress_mt / squish_decompress_mt, _alloc_mt and _file_mt variants, squish_threads(); progress callbacks are aggregated across chunks and stay monotonic - Output depends only on the chunk size (default 16 MiB, min 64 KiB), never on the thread count; inputs no larger than one chunk emit a plain SQ02 stream, and the compress_bound guarantee is preserved via a whole-input stored-mode fallback - Existing decompression entry points read SQ01 transparently; nested SQ01 containers are rejected - CLI: -t/--threads (compress defaults to 1, keeping the ratio-optimal single-block format; decompress uses all cores) and -b/--block MiB; status line/summary now use a wall clock instead of clock() - Threads: pthreads on POSIX (-pthread added to Makefile and squish.pc), native Win32 threads on Windows - The SQ01 magic previously named an unreleased pre-1.0 draft that no reader accepted; FORMAT.md documents the reassignment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cbc42ce commit c32ae6b

10 files changed

Lines changed: 789 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ Per [CONTRIBUTING.md](CONTRIBUTING.md), any change to the model constants in
1111

1212
### Added
1313

14+
- Multi-threaded compression and decompression: new `SQ01` multi-block
15+
container (independent `SQ02` chunk streams; see docs/FORMAT.md §1b) and
16+
library functions `squish_compress_mt` / `squish_decompress_mt`, their
17+
`_alloc_mt` / `_file_mt` variants, and `squish_threads()`. Output depends
18+
only on the chunk size (default 16 MiB), never on the thread count, and
19+
the `squish_compress_bound` guarantee is preserved. Existing
20+
decompression entry points read `SQ01` streams transparently. (The
21+
`SQ01` magic previously named an unreleased pre-1.0 draft no reader
22+
accepted; it has been reassigned to the multi-block container.)
23+
- CLI: `-t N` / `--threads N` — 0 = all cores; compression defaults to 1,
24+
keeping the ratio-optimal single-block format, decompression to all
25+
cores — and `-b N` / `--block N` block size in MiB for `-t`
1426
- CLI: live status line on stderr (percent, bytes, throughput) while
1527
compressing/decompressing when stderr is a terminal; `-q`/`--quiet`
1628
suppresses the status line and the final summary (errors still print)
@@ -20,6 +32,13 @@ Per [CONTRIBUTING.md](CONTRIBUTING.md), any change to the model constants in
2032
- `build-windows.bat`: builds `squish.dll` + `squish.exe` with MSVC from a
2133
plain command prompt (locates Visual Studio via vswhere; no make needed)
2234

35+
### Changed
36+
37+
- CLI: the status line and summary now measure throughput on a wall clock
38+
(was CPU time, which over-counts when threads are in play)
39+
- Building now requires a threads library: `-pthread` outside Windows
40+
(added to the Makefile and pkg-config file), Win32 threads on Windows
41+
2342
## [1.0.0]
2443

2544
### Added

Makefile

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
CC ?= gcc
1313
OPT ?= -O3 -funroll-loops
1414
WARN = -Wall -Wextra
15+
THREADS ?= -pthread
1516
CFLAGS ?= $(OPT)
1617
PREFIX ?= /usr/local
1718
DESTDIR ?=
@@ -30,7 +31,7 @@ all: libsquish.so squish
3031

3132
# ---- shared library ---------------------------------------------------------
3233
libsquish.so.$(VERSION): squish.c squish.h
33-
$(CC) $(CFLAGS) $(WARN) -fPIC -fvisibility=hidden -DSQUISH_BUILD \
34+
$(CC) $(CFLAGS) $(THREADS) $(WARN) -fPIC -fvisibility=hidden -DSQUISH_BUILD \
3435
-shared -Wl,-soname,$(SONAME) -o $@ squish.c -lm
3536

3637
libsquish.so: libsquish.so.$(VERSION)
@@ -39,14 +40,14 @@ libsquish.so: libsquish.so.$(VERSION)
3940

4041
# ---- static library ---------------------------------------------------------
4142
squish.o: squish.c squish.h
42-
$(CC) $(CFLAGS) $(WARN) -c -o $@ squish.c
43+
$(CC) $(CFLAGS) $(THREADS) $(WARN) -c -o $@ squish.c
4344

4445
libsquish.a: squish.o
4546
ar rcs $@ $^
4647

4748
# ---- CLI (statically linked against the lib) ---------------------------------
4849
squish: squish_cli.c libsquish.a
49-
$(CC) $(CFLAGS) $(WARN) -o $@ squish_cli.c libsquish.a -lm
50+
$(CC) $(CFLAGS) $(THREADS) $(WARN) -o $@ squish_cli.c libsquish.a -lm
5051

5152
# ---- Windows DLL + CLI (cross-compile; or use cl.exe /DSQUISH_BUILD_DLL) ------
5253
dll: squish.dll squish.exe
@@ -78,11 +79,11 @@ test: tests/test_squish
7879
./tests/test_squish
7980

8081
tests/test_squish: tests/test_squish.c libsquish.a
81-
$(CC) $(CFLAGS) $(WARN) -I. -o $@ tests/test_squish.c libsquish.a -lm
82+
$(CC) $(CFLAGS) $(THREADS) $(WARN) -I. -o $@ tests/test_squish.c libsquish.a -lm
8283

8384
example: examples/example
8485
examples/example: examples/example.c libsquish.so
85-
$(CC) $(CFLAGS) $(WARN) -I. -o $@ examples/example.c -L. -lsquish -lm \
86+
$(CC) $(CFLAGS) $(THREADS) $(WARN) -I. -o $@ examples/example.c -L. -lsquish -lm \
8687
-Wl,-rpath,'$$ORIGIN/..'
8788

8889
# ---- install ------------------------------------------------------------------
@@ -95,7 +96,7 @@ install: libsquish.so libsquish.a squish
9596
install -m 644 libsquish.a $(DESTDIR)$(PREFIX)/lib/
9697
install -m 644 squish.h $(DESTDIR)$(PREFIX)/include/
9798
install -m 755 squish $(DESTDIR)$(PREFIX)/bin/
98-
printf 'prefix=%s\nlibdir=$${prefix}/lib\nincludedir=$${prefix}/include\n\nName: squish\nDescription: context-mixing compressor\nVersion: %s\nLibs: -L$${libdir} -lsquish -lm\nCflags: -I$${includedir}\n' \
99+
printf 'prefix=%s\nlibdir=$${prefix}/lib\nincludedir=$${prefix}/include\n\nName: squish\nDescription: context-mixing compressor\nVersion: %s\nLibs: -L$${libdir} -lsquish -lm -pthread\nCflags: -I$${includedir}\n' \
99100
"$(PREFIX)" "$(VERSION)" > $(DESTDIR)$(PREFIX)/lib/pkgconfig/squish.pc
100101

101102
clean:

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,22 @@ No dependencies beyond libc/libm.
4646
## CLI
4747

4848
```sh
49-
./squish c bigfile bigfile.sq # compress
50-
./squish d bigfile.sq restored # decompress (checksum-verified)
49+
./squish c bigfile bigfile.sq # compress
50+
./squish d bigfile.sq restored # decompress (checksum-verified)
51+
./squish -t 0 c bigfile bigfile.sq # compress on all cores (multi-block)
52+
./squish -t 0 -b 4 c big big.sq # ... with 4 MiB blocks (more parallel,
53+
# slightly worse ratio)
5154
```
5255

56+
`-t N` compresses with N threads (`0` = all cores) by splitting the input
57+
into independently modeled blocks — near-linear speedup, at a small ratio
58+
cost because each block's model starts cold (about 1–2% at the default
59+
16 MiB blocks, more at smaller `-b` sizes). The default `-t 1` keeps the
60+
ratio-optimal single-block format used for the results table above.
61+
Decompression always uses all cores when the stream allows it (`-t` caps
62+
it) and reads both formats transparently. Budget ~150 MB of model memory
63+
per thread.
64+
5365
When stderr is a terminal, a live status line (percent, bytes, throughput)
5466
is shown while working, followed by a one-line summary. Pass `-q` /
5567
`--quiet` to suppress both; errors are still reported.
@@ -68,8 +80,16 @@ squish_decompress_alloc(c, cn, &d, &dn); /* integrity-checked */
6880
squish_free(c); squish_free(d);
6981
```
7082
71-
Link with `-lsquish -lm` (or `pkg-config --cflags --libs squish` after
72-
`make install`).
83+
Multi-threaded variants take a thread count (0 = all cores) and, for
84+
compression, a chunk size (0 = 16 MiB default):
85+
86+
```c
87+
squish_compress_alloc_mt(data, n, &c, &cn, 0, 0, NULL, NULL);
88+
squish_decompress_alloc_mt(c, cn, &d, &dn, 0, NULL, NULL);
89+
```
90+
91+
Link with `-lsquish -lm -pthread` (or `pkg-config --cflags --libs squish`
92+
after `make install`).
7393

7494
Python needs no wrapper — `libsquish.so` loads directly with ctypes; see
7595
[examples/example.py](examples/example.py).

SQUISH.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ payload · 32-bit checksum of the original data. Incompressible inputs fall
7272
back to stored mode, so output never exceeds input + 17 bytes; decompression
7373
verifies the checksum. Full byte-level spec: [docs/FORMAT.md](docs/FORMAT.md).
7474

75+
The model pipeline is strictly sequential within a stream — every bit's
76+
prediction depends on the previous bit's update — so multi-core operation
77+
comes from cutting the input into chunks instead: an `"SQ01"` multi-block
78+
container wraps independent `SQ02` chunk streams that compress and
79+
decompress in parallel (`squish -t`, `squish_*_mt`), trading ~1–2% of
80+
ratio for near-linear speedup.
81+
7582
## Usage
7683

7784
```

docs/API.md

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,59 @@ from the coding loop, so keep it cheap and do not call back into the
128128
library. `user` is passed through untouched. With `progress == NULL` these
129129
are identical to the plain versions.
130130
131+
## Multi-threaded variants
132+
133+
```c
134+
int squish_threads(void);
135+
```
136+
Number of processors online (>= 1); what `nthreads = 0` selects below.
137+
138+
```c
139+
int squish_compress_mt(const void *src, size_t src_len,
140+
void *dst, size_t *dst_len,
141+
int nthreads, size_t chunk_size,
142+
squish_progress_fn progress, void *user);
143+
int squish_decompress_mt(const void *src, size_t src_len,
144+
void *dst, size_t *dst_len, int nthreads,
145+
squish_progress_fn progress, void *user);
146+
147+
int squish_compress_alloc_mt(const void *src, size_t src_len,
148+
void **dst, size_t *dst_len,
149+
int nthreads, size_t chunk_size,
150+
squish_progress_fn progress, void *user);
151+
int squish_decompress_alloc_mt(const void *src, size_t src_len,
152+
void **dst, size_t *dst_len, int nthreads,
153+
squish_progress_fn progress, void *user);
154+
155+
int squish_compress_file_mt(const char *src_path, const char *dst_path,
156+
int nthreads, size_t chunk_size,
157+
squish_progress_fn progress, void *user);
158+
int squish_decompress_file_mt(const char *src_path, const char *dst_path,
159+
int nthreads,
160+
squish_progress_fn progress, void *user);
161+
```
162+
163+
Parallel counterparts of the functions above, with the same buffer,
164+
allocation, file, and progress contracts. Compression splits the input
165+
into `chunk_size`-byte chunks (`0` = `SQUISH_DEFAULT_CHUNK`, 16 MiB;
166+
minimum `SQUISH_MIN_CHUNK`, 64 KiB) and codes each independently on a pool
167+
of `nthreads` workers (`0` = all cores), emitting a multi-block `SQ01`
168+
stream — see [FORMAT.md](FORMAT.md) §1b. Points worth knowing:
169+
170+
- **Output is deterministic**: it depends on the input and `chunk_size`
171+
only, never on `nthreads`.
172+
- **Ratio cost**: each chunk's model starts cold, costing roughly 1–2% at
173+
the 16 MiB default (more at smaller chunks). Inputs no larger than one
174+
chunk produce a plain `SQ02` stream, bit-identical to `squish_compress`.
175+
- **Bound preserved**: `squish_compress_bound` still holds — inputs that
176+
don't benefit fall back to a single stored-mode `SQ02` stream.
177+
- The decompression functions read both formats; parallelism applies to
178+
`SQ01` streams (`SQ02` has a single sequential model). The plain
179+
(non-`_mt`) decompression functions also accept `SQ01`, serially.
180+
- **Memory**: ~150 MB of model state per active worker.
181+
- `progress` calls are serialized by the library but may arrive on worker
182+
threads; `processed` stays monotonic across the whole input.
183+
131184
---
132185
133186
## Linking recipes
@@ -150,7 +203,7 @@ From Python, load `libsquish.so` with `ctypes` directly —
150203

151204
- The shared library uses semantic versioning; soname `libsquish.so.1`.
152205
Functions are added, never changed, within a major version.
153-
- The bitstream format is identified by the magic (`SQ02`). The model
154-
constants in `squish.c` **are** the format: streams are only decodable by
155-
a build with identical constants. `SQ02` readers reject other magics with
156-
`SQUISH_E_FORMAT`.
206+
- The bitstream format is identified by the magic (`SQ02` single stream,
207+
`SQ01` multi-block). The model constants in `squish.c` **are** the
208+
format: streams are only decodable by a build with identical constants.
209+
Readers reject other magics with `SQUISH_E_FORMAT`.

docs/FORMAT.md

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
# SQUISH stream format "SQ02"
1+
# SQUISH stream formats "SQ02" and "SQ01"
22

33
This document specifies the byte format completely enough to write an
44
independent decoder. Because SQUISH is a context-mixing design, the
55
container is trivial and the real specification is the **predictor**: the
66
decoder must reproduce the encoder's probability for every bit exactly,
77
which means every constant and update rule below is normative.
88

9+
There are two containers: `SQ02`, a single model stream, and `SQ01`
10+
(§1b), a multi-block wrapper holding independent `SQ02` streams so that
11+
chunks can be coded in parallel.
12+
913
## 1. Container
1014

1115
```
@@ -24,6 +28,36 @@ last 4 checksum: FNV-1a 64 of the n original bytes, low 32 bits, LE
2428
- `mode 0`: payload is the arithmetic-coded bitstream described below.
2529
- FNV-1a 64: `h = 0xcbf29ce484222325; for each byte b: h ^= b; h *= 0x100000001b3`.
2630

31+
## 1b. Multi-block container "SQ01"
32+
33+
Produced by the multi-threaded encoder for inputs larger than one chunk.
34+
The payload is a sequence of complete, independent `SQ02` streams; models
35+
never carry state across chunk boundaries, which is what makes parallel
36+
encode and decode possible.
37+
38+
```
39+
offset size field
40+
0 4 magic: 'S' 'Q' '0' '1'
41+
4 8 total original size n, unsigned 64-bit little-endian
42+
12 1 mode: 2 = multi-block
43+
13 4 chunk count k >= 1, unsigned 32-bit little-endian
44+
17 4*k compressed size of each chunk, u32 LE each
45+
17 + 4*k ... k chunks, each a complete SQ02 stream (§1)
46+
```
47+
48+
- Chunk i decodes to bytes `[sum of previous chunks' sizes ...)` of the
49+
output; the original size of each chunk comes from its own SQ02 header.
50+
- The compressed sizes must tile the rest of the file exactly, and the
51+
per-chunk original sizes must sum to n.
52+
- Chunks must be `SQ02` streams: nested `SQ01` containers are invalid.
53+
- There is no container-level checksum; each chunk carries its own (§1).
54+
- Encoders chunk the input at a fixed chunk size (encoder choice; the
55+
reference default is 16 MiB, minimum 64 KiB), so the emitted stream
56+
depends only on the input and the chunk size — never on the thread
57+
count. An encoder that finds the multi-block stream no smaller than
58+
stored mode emits a single stored-mode `SQ02` stream instead, which
59+
preserves the n + 17 output bound.
60+
2761
## 2. Arithmetic coder
2862

2963
Carryless binary range coder over 32-bit registers, coding bits MSB-first
@@ -160,7 +194,10 @@ bit — predict, code, update, in that order, for encoder and decoder alike.
160194

161195
## 10. Versioning
162196

163-
`SQ02` is the only current version. Any change to a constant, table size,
164-
initialization value, or update rule in §§2–9 produces incompatible streams
165-
and requires a new magic. (`SQ01` was a pre-release format without the mode
166-
byte and checksum; it is not accepted by `SQ02` readers.)
197+
`SQ02` (single stream) and `SQ01` (multi-block, §1b) are the current
198+
versions. Any change to a constant, table size, initialization value, or
199+
update rule in §§2–9 produces incompatible streams and requires a new
200+
magic. (The magic `SQ01` previously named a pre-release single-stream
201+
format without the mode byte and checksum; that format was never released
202+
and no reader accepted it, so the magic has been reassigned to the
203+
multi-block container, which is distinguishable by its mode byte 2.)

0 commit comments

Comments
 (0)