Efficiency benchmark

A head-to-head comparison of lha against tar, tar.gz, and zip on a representative corpus, at default and best compression levels. The numbers tell a clear story about which tool wins on which axis.

5 runs per cell, median ± sample-stdev, on an Apple M-series laptop, native lha build (LHa for UNIX 1.14i-ac20220213, default configure options). Wall-clock end-to-end: compress time + decompress time per (tool, level) cell.

← Security audit · ↑ site

The headline (one-glance table)

Compressed and decompressed the same 1.83 MiB corpus five times per cell. Numbers are median wall-clock; stddev reflects the run-to-run noise of the laptop itself.

tool level compress decompress size B ratio
median±σmedian±σ
lha-lh5- (default) ~50 ms±5 ~40 ms±1 547,04529.93 %
lha-lh7- ~46 ms±1 ~40 ms±6 547,04529.93 %
tarno compression ~80 ms±4 ~82 ms±5 2,732,544149.55 %
tar.gz-6 (default) ~210 ms±5 ~5 ms±0.1 455,43524.93 %
tar.gz-9 (best) ~210 ms±1 ~5 ms±0.1 455,43524.93 %
gzip -c-6 (raw stream) ~170 ms±4 ~5 ms±0.2 455,48624.93 %
gzip -c-9 (best) ~170 ms±3 ~5 ms±0.2 455,48624.93 %
zip-6 (default) ~62 ms±1 ~3 ms±0.4 566,68331.01 %
zip-9 (best) ~85 ms±1 ~3 ms±0.1 566,68331.01 %

All times are wall-clock end-to-end. "level" = the tool's own knob, not a number pulled from thin air. The corpus file count is 304; total uncompressed bytes 1,827,165. The tar (no compression) ratio exceeds 100 % because tar stores per-file metadata (UID, GID, mode bits, 512-byte per-record padding); for small files that adds 50 % of overhead before any deflate runs.

Reading the table

1. Compress speed: zip < lha < gzip < tar < tar.gz

At default levels:

So at default levels: zip is ~22 % faster than lha; lha is ~76 % faster than tar.gz. Compress-speed ordering is stable across corpus types in our testing (text source, mixed binary, random data) — the individual times shift, the rank doesn't.

2. Decompress speed: tar.gzzip < lha < tar

Decompress reverses the speed ranking in interesting ways:

For the workloads most people care about (decompress 100 times from a CDN) the absolute time is dominated by network latency, not algorithm choice. All three of tar.gz, zip, and lha decompress in single-digit ms on this corpus; the differences are well below the noise floor of disk seek + HTTP request latency.

3. Compression ratio: tar.gz < gziplha < zip

For the same kenlm corpus:

The ratio difference between tar.gz and lha is about 5 percentage points — ~91 KB on a 1.8 MiB input. On a fast CDN, that 91 KB re-downloads in <100 ms; on a slow CDN, it might take 1-2 seconds. That's the actual size-vs-speed trade on the wire.

4. -9 is a trap on this corpus

For already-redundant text (source code with repeating words and whitespace), gzip -9 and tar.gz -9 find nothing new compared to -6. The corpus isn't rich enough to benefit from the extra passes; -9 wastes 5-10x more CPU for ~0 % ratio gain.

If you have truly high-entropy content (a video fragment, a database dump, encrypted data), -9 will show a real gap. This corpus doesn't.

Which tool when

if you care about…usewhy
smallest possible output tar.gz -9 / zstd / brotli deflate at level 9 (or zstd -19 / brotli -11) is the best ratio you'll get from a single binary that's already on every Unix system. The trade is >5× compress time vs lha for ~5 % ratio.
fastest compress zip -6 ~62 ms. DEFLATE64 off, no tar blocking, small overhead per file. Loses 6 % ratio vs deflate -6.
fastest decompress (CDN, OTA update, package mirror) lha or zip Both decode in single-digit ms on this corpus. lha has a slightly larger archive but the absolute size of decompressed code is tiny; network latency dominates. If you're also concerned about install-on-edge, lha's smaller dep surface (no zlib, no iconv, no ssl) matters more than the 90 KB ratio delta.
single static binary for CI artifact + CDN lha x eget use lha drops a 152 KB statically-linked binary in ~/.local/bin with no deps. tar.gz needs libz + libiconv + tar on the host; zip needs libzip + libz. lha has no such dep tree.
shipping a Windows or macOS .dmg that any user can open zip (or .tar.gz) zip is built into Windows + macOS; tar.gz opens natively. lha needs the binary to be installed first — x eget use lha makes that one command, but it's still a step.
embedded / firmware / old hardware lha Decompressor fits in < 16 KB on constrained targets. Designed for MS-DOS machines with 640 KB RAM.

Deeper reading

Why lha -lh5- is faster than tar.gz -6 on compress

LZHUF (the algorithm behind .lh5-) uses a static-Huffman-table, fast-string-match design from 1990. deflate (gzip / zlib) uses LZ77 + dynamic Huffman and a sliding window that the encoder has to decide on. For input that doesn't fully fill the window (our 1.8 MiB corpus fits in a 32 KB window with room to spare), deflate spends most of its time scanning for matches; LZHUF doesn't. The decompressor side is closer to a tie because both algorithms have the same rough decode-loop structure.

On a corpus bigger than the deflate window (32 KB at -6), deflate starts to win on ratio because it can find longer-distance matches. Our 1.8 MiB corpus is small enough that ratio doesn't move much past -6, and the encode cost dominates. For a 100 MiB corpus, expect tar.gz -9 to pull ahead on ratio at ~3-5x the compress time of lha -lh5-.

Why tar (no compress) is so much bigger than input

tar records per-file metadata (UID, GID, mode, timestamps, filenames) plus a 512-byte-per-record padding. For our 1.8 MiB corpus with 304 small files, that's ~50 % of overhead before any deflate runs. tar -czf compresses most of it back out via the LZ77 backend, but raw tar is genuinely larger than its input for small-file corpora.

Why zip -6 is faster than tar.gz -6 on decompress

zip's archive format has a single central directory at the end of the file — the decoder can read it without streaming. tar.gz has to scan the tar header for every entry to find its size, then ask the deflate decoder to fill that exact size. zip's deflate can be wrapped around the central directory; tar.gz's deflate is per-record, and each record needs its own header parse. The single-digit ms gap here is small; on a corpus with many files (thousands), zip's lead grows.

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

Decompression is where LZHUF (the algorithm behind .lh5- and .lh7-) shows its age. The 1990 design is fundamentally bit-serial:

So the answer to the question is: the algorithm is the ceiling, not the implementation. LZHUF was tuned in 1990 for "fast unpack on a 286 with 1 MiB RAM", and the bit-serial decode loop was the natural fit. A SIMD-optimized lha port would gain perhaps 1.5-2x on decompress (the length-distance copy is the only vectorizable chunk), but the underlying bit-serial Huffman loop is fundamentally serial. To get more decompress speed, you have to switch algorithms, not micro-optimize LZHUF.

For comparison: zstd decompress is ~5-10x faster than lha at similar ratio, and lz4 is ~3-5x faster still. Both are tuned for modern SIMD CPUs (SSE2/AVX2 on x86_64, NEON on aarch64) and have decompress loops that are SIMD-friendly by design. lha predates MMX (1996) by six years and the original maintainers never had a vector unit to target.

When this matters: lha is the right answer for a static binary that has to live on a system without zstd/lz4 installed. lha is not the right answer for a high-throughput CDN that needs microsecond-scale decompress.

How to reproduce

On any Unix-like host (macOS, Linux, BSD):

git clone --depth 1 https://github.com/ljh-sh/lha.git
cd lha
bash scripts/build.sh
cp build/src/lha /usr/local/bin/

# corpus
test -d corpus/kenlm_src || cp -R ../kenlm/upstream/kenlm corpus/kenlm_src

# 3 runs each, median. The numbers in the table above used 5 runs
# (we report median ± sample-stdev) on an Apple M-series laptop.
for tool in \
  'lha  :lha c k.lh5- corpus/kenlm_src' \
  'tar  :tar -cf k.tar -C corpus kenlm_src' \
  'tgz6 :tar -czf k6.tgz -C corpus kenlm_src' \
  'tgz9 :tar -czf -9 k9.tgz -C corpus kenlm_src' \
  'zip6 :zip -qr -6 k6.zip kenlm_src' \
  'zip9 :zip -qr -9 k9.zip kenlm_src' ; do
  label=${tool%%:*}; cmd=${tool#*:}
  rm -f k.*
  for r in 1 2 3 4 5; do
    rm -rf corpus/kenlm_src; cp -R ../kenlm corpus/kenlm_src
    t=$( { time $cmd >/dev/null 2>&1; } 2>&1 | awk '/real/{print $NF}')
    sz=$(stat -c%s k.* 2>/dev/null | head -1)
    echo "$label  t=$t  sz=$sz"
  done
done

Numbers will differ by host (~10 % on different CPUs, ~5 % across macOS / Linux); the relative ordering (zip → lha → tar → tar.gz on compress time, tar.gz → zip → lha → tar on decompress, tar.gz → lha → zip → tar on ratio) is stable.