Algorithm — history, performance, current positioning
lha ships LZHUF: 1990-era
LZ77 + dynamic Huffman, designed for fast unpack on a 286 with
1 MiB of RAM, written before SIMD was a thing. This page is
about the algorithm, what it costs in 2026, and why a single
static binary of it is what the rest of the world
wires up against.
1. The algorithm
Every compression method in lha's family
(-lh0- through -lh7-, plus
-lhd- for directory headers) shares the same
two-stage shape:
- LZ77-style string matching. Find repeated byte
sequences in the input and emit back-references of the
form
(distance, length)instead of repeating the bytes. The "LZ" in LZARI / LZHUF / ZIP / gzip / zstd / lz4 / 7-zip is all this same idea, going back to Lempel & Ziv's 1977 paper. - An entropy coder on the literals and the
back-references. This is the part that varies:
-lh1-— LZARI: LZ77 + arithmetic coding (Okumura, 1988).-lh5-— LZHUF: LZ77 + dynamic Huffman (Tagawa, 1990). The default.-lh4-,-lh6-,-lh7-,-lzs-— experimental refinements on the entropy backend or unused encodings kept for compatibility.
Static vs dynamic Huffman
LZHUF uses static Huffman tables per block, re-derived every few KB of input. This is the design quirk that defines the entire trade-off:
- Pro. The encoder doesn't have to ship the Huffman table in the bitstream. The decoder has known tables per block and decodes in a tight inner loop with no per-block table-load overhead.
- Con. When local statistics shift — a header block, then a body of structured text, then a binary blob — a static table is locally suboptimal. A dynamic Huffman (deflate's default, what gzip uses) carries the table with the data and adapts. That's where the ~5 % ratio gap on the bench comes from.
The bit-serial decode loop (and why it's slow)
LZHUF's decode loop reads the bitstream one bit at a time, looking up the next symbol via a small 2× table walk. This is fundamentally serial: bit n+1 cannot be read until bit n has been looked up.
The ANSI C source is straight 1995-2003 code with no SIMD intrinsics:
$ grep -nEi '(mmx|sse|avx|simd|__m128i|__builtin_ia32)' upstream/lha/src/*.c
$ # zero hits
Vectorizing the LZ77 length-distance copy would get you maybe 1.5–2× decompress speedup; the bit-serial Huffman loop is the ceiling. To go faster on decompress you have to switch algorithms, not micro-optimize LZHUF. (See the perf page for the longer version.)
2. History — the lineage
The history section on the home page covers the 1988-2022 arc in detail; the compact version:
- 1977 — LZ77. Abraham Lempel & Jacob Ziv publish A Universal Algorithm for Sequential Data Compression in IEEE Transactions on Information Theory. The sliding-window + back-reference idea that defines every modern archiver.
- 1988 — LZARI. Haruhiko Okumura publishes a CPU-friendly LZ77 + arithmetic coding variant. The 286-era arithmetic coder that LHA borrows.
- 1988–1989 — LHarc. Haruyasu Yoshizaki (Y.Tagawa) wraps LZARI in an MS-DOS archive format. First widely-used archive format in Japan.
- 1990 — LZHUF, the format we ship. Tagawa
refines LZARI into LZ77 + dynamic Huffman, renames the
format from LHarc to LHA. The
compression-method-suffix convention (
-lh5-in archive filenames) is born; it lives to this day. - 1991–1993 — LHA for UNIX. Masaru Oki ports to UNIX; Nobutaka Watazaki rewrites the codebase. The canonical C source that's been vendored for 30 years.
- 1993 — PKZIP 2.0 + Info-ZIP displace LHA on Western PCs.
- 1994–2010 —
.lzhstays hot in Japan. Major sites distribute in LZH. Windows SFX archives use LHA extensively. J2K-style bulletin boards exchange LZH into the late 2000s..lzhis the dominant distribution format in Japan through the early 2000s. - 2022 — autoconf revival. GitHub user
jca02266
picks up the dormant tree, adds a modern autotools
build, tags LHa for UNIX 1.14i-ac20220213. This
is the source vendored at commit
86094cbin this repo. - 2026 — ljh-sh/lha. Vendors
jca02266/lha@86094cb; ships a single static binary for every major platform. The reason for being here today is in §4.
3. Performance — the three axes
Numbers from the efficiency bench on the 1.83 MiB kenlm source corpus, 5 runs per cell, median ± sample-stdev. The cliffnotes:
| tool | level | compress | decompress | ratio |
|---|---|---|---|---|
| lha | -lh5- (default) |
~50 ms | ~40 ms | 29.93 % |
| zip | -6 (default) |
~62 ms | ~3 ms | 31.01 % |
| tar (no compression) | — | ~80 ms | ~82 ms | 149.55 % |
| tar.gz | -6 (default) |
~210 ms | ~5 ms | 24.93 % |
Three observations that matter
- Compress speed.
lhasits betweenzip(fastest, ~22 % lead) andtar.gz(slowest, ~4× slower). For workloads that compress-then-store (build artifact retention, package mirrors),lhais the cheapest non-trivial compressor that still beats the wire by ~3×. - Decompress speed.
zipandtar.gzdecompress in single-digit ms;lhatakes ~40 ms. The bit-serial Huffman loop is the ceiling. The absolute time is still tiny — well below network-RTT noise on any CDN. Workloads that re-decompress from a CDN 100 times are dominated by HTTP latency, not algorithm choice. - Ratio.
lhalands ~5 percentage points abovedeflate(~91 KB larger on 1.8 MiB of source code). This is the deliberate 1990 trade-off: tuned for fast unpack on a 286, not for the tightest compression on a 2020s workstation.
Where LZHUF loses — and where it doesn't
- ✗ On ratio,
deflateandzstdwin by 5–15 percentage points. - ✗ On decompress,
zstdis ~5–10× faster thanlha;lz4is ~3–5× faster still. - ✓ On static-binary footprint + zero-dep shipping,
lhawins outright: ~150 KB and runs on anything with a kernel. - ✓ On "I have an archive from a 1994 game and nothing else
opens it,"
lhawins by default — it's the only thing that can. - ✓ On long-term archival stability,
lhawins by virtue of its 35-year-old format spec still being implemented and reader-compatible.
The deeper reading section of the bench page goes through algorithm-internal reasons. The short version: lha is a 1990 design that hasn't changed, and the speed ceiling is the algorithm, not the implementation.
4. Current positioning — why this project exists in 2026
The vendored jca02266/lha@86094cb source is a
1995-2003 ANSI C codebase that's had one modern-build-system
pass in 2022. ljh-sh/lha takes it from there and builds one
thing: a single static binary of lha for every
platform, so users don't need a compiler.
There are three concrete demand surfaces for that proposition in 2026:
4.1 Legacy .lzh unpacking
The most obvious: someone has a 1994 game patch, a J2K-style
BBS legacy attachment, or a Japanese open-source archive from
the 2000s. Nothing modern opens .lzh. With
x eget use lha you have it in
~/.local/bin in two seconds and
lha x foo.lzh works. Without the binary, the
user has to install a compiler, find a 1990s-vintage autoconf
patch, and pray their libc hasn't broken the build.
4.2 Single static binary for CI artifact / CDN distribution
lha produces a single .lzh archive
from any source tree in ~50–80 ms with zero system deps
on the host. For CI artifact publishers (anyone packaging
build output for download), this means:
- No
libz,libcurl,libiconv,libsslto drag into a minimal container. - One ~150 KB binary to vendor, sign, and ship.
- A decompress path that works on Windows, macOS, Linux,
BSD, and any embedded Linux alike — because the
binary runs from
~/.local/binwithout a system install.
This is the core "static binary distribution convention" that ljh-sh adopts across projects (kenlm, upxz, now lha). See the build audit for what that pipeline looks like.
4.3 x-cmd's zuz module — the direct motivation
This is the reason this project ships as a static build.
x-cmd's
zuz module does format auto-detection on archive
files: pick the right tool by extension, decompress or
compress transparently. It supports tar,
tar.gz, tar.bz2, tar.xz,
zip, 7z, rar,
lha, etc. But it has one architectural rule:
Never bundle a format-specific tool. Always shell out to a system-installed binary.
The problem landed in
x-cmd
issue #131 (December 2024): a user wants LHA/LZH
support in zuz, but no portable package system
x-cmd supports currently ships lha. The thread
unfolds as:
- Dec 2024: the maintainer (edwinjhlee) notes that v0.5.1
shipped 7z-based decompression as a fallback
(which works as long as the user has either
lhaorlhasainstalled). - Jan 2025: the maintainer points users at
x install lhaas the workaround path until a packagedlhamodule exists. - Jul 2026: the maintainer
closes
the loop:
zuzwill lazy-download this very repo's release artifact, sox uz foo.lhaworks on any system without a pre-installedlha.
That means the project's primary install path for
zuz users is:
x uz foo.lha
# zuz detects .lha, sees no lha in PATH,
# falls back to: eget ljh-sh/lha --to ~/.local/bin/lha
# then: ~/.local/bin/lha x foo.lha
Not Homebrew. Not apt. Not a per-distro package. One default static install that doesn't require a privileged install, doesn't require a tap, doesn't require a build chain, and works on every platform x-cmd supports. That's the version of "default static install capability" this repo exists to provide.
If you use x-cmd and ever do x uz foo.lha on a
system with no lha, you're using this binary.
If you've never used x-cmd and just need to open a
.lzh, you're also using this binary — same
artifact, same release, same vendored source.
4.4 Where this isn't positioned
To be honest about the boundary:
- Not an algorithm upgrade. There's no tANS port, no LZMA-lha variant, no tANS-for-LZH format. The vendored 1.14i stays untouched.
- Not a replacement for
tar.gz,zip, orzstd. The bench says clearly when to reach for which. - Not a sign of LZH making a comeback. The
format's longevity is the only reason this binary
exists; if your workflow started after 2010, you
probably don't have
.lzhfiles.
5. Summary
If you want one rule of thumb:
| if you care about… | reach for |
|---|---|
opening a .lzh from 1994 |
lha (this binary) |
| one static binary on a closed system | lha (~150 KB, no deps) |
zuz-style format auto-detection |
lha (lazily fetched from ljh-sh/lha) |
| smallest artifact for a long-tail CDN | tar.gz -9 or zstd |
| decompressing at microsecond scale | lz4 or zstd |
This page exists because "what is this binary" and "why does it exist" deserve one place, not scattered across the history, bench, build audit, and FAQ pages.