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.

The algorithm · History · Performance · Today · Summary

1. The algorithm

Every compression method in lha's family (-lh0- through -lh7-, plus -lhd- for directory headers) shares the same two-stage shape:

  1. 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.
  2. 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:

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:

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:

toollevelcompress decompressratio
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

  1. Compress speed. lha sits between zip (fastest, ~22 % lead) and tar.gz (slowest, ~4× slower). For workloads that compress-then-store (build artifact retention, package mirrors), lha is the cheapest non-trivial compressor that still beats the wire by ~3×.
  2. Decompress speed. zip and tar.gz decompress in single-digit ms; lha takes ~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.
  3. Ratio. lha lands ~5 percentage points above deflate (~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

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:

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:

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:

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.