FAQ

Performance, format, future formats, and the lha-rs question. A consolidated record of the analysis behind the static-binary distribution.

Decompress speed · tANS competitive range · zstd -1 as "best" · lha-rs · lha vs bzip2 / 7z · Where new formats live

Why is lha's decompress so much slower than deflate / zstd / lz4?

Short answer: the algorithm is fundamentally bit-serial and there's zero SIMD in the source. ~3-7× speedup is available with non-SIMD algorithmic changes; matching zstd -1 is a multi-year project.

The two hot paths in the source

Decoding LZHUF means:

Why this is fundamentally slow

What you can do without changing the on-disk format

OptimizationWhereDecoded speed gain
Pre-computed symbol lookup table (zlib-style inflate) replaces bit-by-bit tree walk in src/huf.c 3-5× on the bit-loop part
memcpy(dst, src, len) for LZ77 back-ref copy replaces per-byte loop in src/slide.c 2-4× on the copy part
Branchless 16-bit table lookup same code path as #1, more aggressive 5-7×
Two-stage pipeline (read buffer + decode chunks) rewire src/dhuf.c and src/slide.c 1.5-2×
Cache hot back-reference distances (top-3 MRU) new in src/slide.c 1.2-1.5×

Combined: 3-7× decode speedup, taking lha -lh5- from ~45 MB/s to ~150-300 MB/s on the kenlm corpus — competitive with deflate level 1-3 (~300-500 MB/s on the same corpus), but not "best in class."

Hard ceiling: what tANS buys (and doesn't)

To match zstd -1 (~2 GB/s) you'd need a fundamentally different entropy coder (tANS/FSE) and a faster LZ77. That means changing the on-disk format — see "Where should new formats live?" below. lha -lh5- on today's C decoder is the limit; no algorithmic trick within LZHUF gets above ~300 MB/s decode.

Source citation

The bench numbers come from a 5-run, kenlm upstream source (1.83 MiB / 304 files) bench, Apple M-series, native lha build. Full table on the perf page. The 5-axis source audit on the security page confirms the bit-loop and per-byte copy are the only two decode-side hot paths.

Could tANS make lha competitive with modern codecs?

Short answer: yes, with tANS, lha lands in the same league as deflate / brotli. Not in the same league as zstd -1+ (multi-year optimization gap) and not in the same league as lz4 / snappy (no entropy coder at all — different architecture).

Where tANS lands against each competitor

OpponentDecode speed Why tANS helps / doesn't
zstd -1~2 GB/s Yann Collet has been tuning FSE/tANS for 8+ years. A "first version" tANS implementation in lha-rs would reach ~1-1.5 GB/s, ~30-50 % behind zstd -1. Multi-year iteration needed to catch up.
zstd -3+~1 GB/s Even further behind; optimal parser is the hard part.
deflate -1 to -3500-800 MB/s Same league. tANS gets to ~500-800 MB/s, deflate does too. Within a small constant factor.
brotli -1300-500 MB/s Slightly ahead on a fresh tANS implementation (brotli is static-Huffman; tANS is faster at the same ratio).
lz4 / snappy / LZO3-5 GB/s Never competitive. These have no entropy coder — they store LZ77 matches in 4-byte token format and decode to ~1 byte/cycle. LHA-with-tANS has entropy-coder overhead lz4 doesn't, so it can't match that speed class by architecture.

The fundamental limit

A decoder is bottlenecked by memory bandwidth + branch mispredicts + cache misses. tANS minimizes branch mispredicts (one branch per symbol). lz4 minimizes cache misses (literal copy in 16-byte chunks). Modern CPUs hit ~1 byte/cycle for the simplest decoders; tANS is around 1-2; lz4 is around 1. The architectures trade these two bottlenecks; one can't dominate the other from outside.

What lha's actual competitive position is

Adding tANS gives lha "competitive with deflate / brotli" status, not "beating zstd -1+" status, and definitely not "beating lz4" status. The real value of lha in 2026 is not speed — it's 35-year format stability + single-file static binary with no deps. Speed is the price of that. So the question becomes: do we add tANS as a new format (.lh8-) to give speed-conscious users an option, while keeping .lh5- as the stable read-only compatibility target? See "Where should new formats live?".

Is zstd -1 "the best" zstd level?

Short answer: yes, zstd -1 is the fastest zstd level (lowest compression ratio, highest speed). But it's not "best" in the sense of "best default" — that would be zstd -3.

zstd's level table

zstd uses zstd -N for levels 1-22, or zstd --fast=N for "faster than default."

LevelRole Ratio vs default -3 Encode speed vs default -3
-1fastest ~5-10 % worse~2-3× faster
-3default — speed / ratio sweet spot 1.0×1.0×
-6balanced ~5-8 % betterslightly slower
-10prefer compression ~10-15 % better3-5× slower
-15high compression ~20-25 % better8-10× slower
-22max ~30-50 % better30-50× slower

Why -3 is the actual default

The speed/ratio curve is not linear:

Hence zstd's author (Yann Collet) chose -3 as the default: it's the shoulder of the curve. -1 is too aggressive on ratio for most use cases; -10 wastes time without much gain.

But zstd -1 isn't the fastest decoder overall

zstd -1 is the fastest zstd level, but:

CompressorDecode speed Has entropy coding?
lz4~3-4 GB/sno
snappy~3-5 GB/sno
zstd -1~1.5-2 GB/syes (FSE / tANS)
zstd -3~1 GB/syes
brotli -1~300-500 MB/syes (static Huffman)
deflate -1~500-800 MB/syes (dynamic Huffman)
lha -lh5- (current)~45 MB/syes (static Huffman)

lz4 / snappy are 1.5-2× faster than zstd -1 because they have no entropy coder at all. So "fastest decoder" is a two-track race, and lz4 wins the no-entropy track; zstd -1 wins the entropy-track.

What are lha's unique features vs bzip2 / zip / 7z?

Short answer: lha's "features" are mostly not features — they're trade-offs that bought a 35-year stable format. But lha has some genuine uniqueness: per-file independent compression, stdio-stream friendly, and streaming-buildable catalog.

What lha does not have

Featurebzip2zip7zlha
Block-random access (seek to 100 KB, decode 1 KB) ✅ 100-900 KB blocks ⚠️ per-file only ✅ blocks ❌ (stream)
Global index file ✅ central dir at end ❌ (but streaming-buildable)
Sparse files / hardlinks / ACL / xattr partial
Strong checksum (SHA-256) ❌ (CRC32) ❌ (CRC32)
Compression dictionary / pretrain

lha is a stream-oriented (not block) archiver — same shape as zip / gzip / tar, not bzip2 / 7z. So bzip2's "seek to byte 100 MB and decode 1 KB" is structurally impossible with lha -lh5-.

What lha does have that's unusual

What this means strategically

lha's value proposition in 2026 is format stability + zero deps + tiny size + 35-year portability, not "rich feature set." To get bzip2-style features (random access, index, strong checksum) the right path is new formats .lh5b- (block), .lh8- (tANS), and .lhi- (index file) — not a Rust rewrite of lha. See next question.

Could a Rust rewrite help lha? (the lha-rs question)

Short answer: no. lha-rs is not worth it as a 1:1 Rust replacement of the C version. The honest answer: most of the standard reasons for a Rust rewrite don't apply to lha specifically.

Each standard "rewrite in Rust" reason, applied to lha

"Reason"Reality for lha
Memory safety (Rust eliminates strcpy/sprintf risks) We just did a 5-axis audit. 8 strcpy sites are all bounds-checked; 0 sprintf / gets / scanf. The vulnerable-class doesn't exist in this codebase. Re-implementing the whole thing to remove a class of bugs that aren't present is poor cost / benefit.
cargo-fuzz / cargo test infrastructure libFuzzer attaches to any C / C++ project, not just Rust. oss-fuzz provides free hosting for C projects. Adding libFuzzer to lha is 1-2 weeks of work, not a separate repo.
Modern error handling (Result<T, E>) C can return a tagged-union error. The real work is the error vocabulary, not the language. 1 week of refactoring lha's error path, not 1 year.
Modern CLI (clap / argh) lha's CLI is stable since 1990. Refactoring it is a breaking change, not an improvement. The CLI is not lha's pain point.
Pluggable compression crates (flate2, zstd-rs, lz4_flex) Those are for new projects. They don't need to be in lha-rs to be useful — anyone writing a new Rust tool uses them already.
Algorithm optimization carrier Wrong layer. The pre-computed-table + memcpy optimizations are language-agnostic. Write them in C — they fit in the existing CI matrix, no new project.
New format experiments (tANS, etc.) This is a new-format problem, not a lha-rs problem. New formats live in lha/ as src/lh5b/, src/lh8/. Not a separate repo.

When a Rust rewrite is actually right

One of these conditions must hold:

None of these applies to lha today.

What the right move is

  1. Stay in lha (C) — maintain and audit. 5-axis security audit done; libFuzzer integration 1-2 weeks away; oss-fuzz entry 1-2 hours. Done.
  2. Add new formats in C as subdirectories: src/lh5b/ (block format with random read), src/lh8/ (tANS), src/lhi/ (index file). C implementations get the existing CI matrix; no new repo, no new release cadence.
  3. Write a format spec at docs/lzh-format.md so any future implementation (Rust, Python, Go) has a single source of truth.
  4. If new formats prove useful, then a separate ljh-sh/lha-spec (or similar) for multiple reference implementations in different languages. Not a "rewrite" repo — a multi-impl test-bed.

lha-rs is not on this list. It would be a 6-12-month project to rewrite a 13,400-line C program whose safety profile is already good, in a language whose value proposition doesn't help. Better: keep the C, do the small additional work in C, and reserve Rust for the day a new format genuinely needs it.

Where should new formats (.lh5b- / .lh8- / .lhi-) live?

Short answer: in the existing ljh-sh/lha repository, not in a new one. New formats are additions to the lha family, not replacements for it.

Proposed new formats

SuffixBackend Compatibility with .lh5- What it adds
.lh5- (current) LZHUF + per-byte copy — (canonical) 35-year stable format; tiny binary; zero deps
.lh5b- Same as .lh5-, but with 256 KB blocks Old .lh5- readers can still extract; new readers gain block random-read Random access within a file; partial extraction (close to bzip2 behavior)
.lh8- tANS (FSE) + faster LZ77 New format, not readable by old .lh5- decoder Decode speed competitive with deflate -1; better ratio than .lh5- on most corpora
.lhi- Index file (separate from archive) Works with any .lh5- (or .lh8-) archive Streaming-buildable catalog; lets lhacat archive.lzh list every file without reading any body

Where each lives

Single binary that handles all of them

The killer feature of doing it in the existing repo: a single ljh-sh/lha binary that reads all of these formats:

$ lha l archive.lh5-          # canonical
$ lha l archive.lh5b         # block random-read
$ lha l archive.lh8-          # tANS, faster decode
$ lha l --index archive.lhi   # streamed catalog
$ lha x archive.lh8-          # write a new tANS archive

Single static binary, ~250 KB on disk, zero deps. The same x i lha / x eget use lha install path. 35-year stable read path + modern tANS write path + block random read. This is lha's competitive position in 2026: a single binary that does what no other archiver can do, and weighs 250 KB.