性能对比

在 1.83 MiB 的代表性语料上对 lhatartar.gzzip 做压缩与解压对比, 每个工具都跑了默认和最佳两级。数字清晰地说明了每个工具 在哪一项上领先。

每格 5 次取中位数,附样本标准差。机器:Apple M 系列, 原生 lha 编译产物(LHa for UNIX 1.14i-ac20220213,默认 configure 选项)。Wall-clock 端到端:每格的 compress + decompress 总时间。

← 安全审计 · ↑ 站点

一眼看完的表

同一 1.83 MiB 语料,压缩解压各 5 次,取中位数。标准差反映 笔记本本身逐次运行的噪声。

工具 级别 compress decompress size B 压缩率
中位±σ中位±σ
lha-lh5- (默认) ~50 ms±5 ~40 ms±1 547,04529.93 %
lha-lh7- ~46 ms±1 ~40 ms±6 547,04529.93 %
tar无压缩 ~80 ms±4 ~82 ms±5 2,732,544149.55 %
tar.gz-6 (默认) ~210 ms±5 ~5 ms±0.1 455,43524.93 %
tar.gz-9 (最佳) ~210 ms±1 ~5 ms±0.1 455,43524.93 %
gzip -c-6 (原始流) ~170 ms±4 ~5 ms±0.2 455,48624.93 %
gzip -c-9 (最佳) ~170 ms±3 ~5 ms±0.2 455,48624.93 %
zip-6 (默认) ~62 ms±1 ~3 ms±0.4 566,68331.01 %
zip-9 (最佳) ~85 ms±1 ~3 ms±0.1 566,68331.01 %

所有时间均为 wall-clock 端到端。"级别"是各工具自带的 压缩档位。语料文件数 304,未压缩总字节 1,827,165。 tar (无压缩)的压缩率超过 100% 是因为 tar 存储每个文件的元数据(UID、GID、mode 位、512 字节 每记录 padding);对零碎小文件来说,没经过 deflate 之前 就已经多了 50%。

怎么读这张表

1. compress 速度:zip < lha < gzip < tar < tar.gz

在默认级别下:

在默认级别上,ziplha快 22%, lhatar.gz快 76%。 compress 速度的相对排序在不同语料上稳定。

2. decompress 速度:tar.gzzip < lha < tar

解压方面速度排序很有趣:

在 CDN 上解压 100 次的真实场景里,绝对解压时间被 网络延迟主导。三种工具的解压时间都在个位数毫秒级; 差异远低于磁盘寻道 + HTTP 请求的噪声底限。

3. 压缩率:tar.gz < gziplha < zip

同一个 kenlm 语料上:

tar.gzlha 的差距约 5 个百分点 — 在 1.8 MiB 上就是约 91 KB。在快速 CDN 上重传 这 91 KB 不到 100 ms;在慢速 CDN 上要 1-2 秒。这就是 实际网线上的 size-vs-speed 权衡。

4. -9 在本语料上是个坑

对于已经具有相当冗余性的文本(源码里有大量重复的 关键字和空白),gzip -9 和 tar.gz -9 都比 -6 找不出 任何新的优化。语料丰富度不够让更高级别发挥作用;-9 在 compress 时间上贵了 5-10 倍,但压缩率提升 ≈ 0%。

如果你有真正高熵的内容(视频片段、数据库 dump、加密数据), -9 会拉开有意义的差距。本语料不满足这个条件。

什么场景用什么工具

如果关心的是…原因
最小可能的输出 tar.gz -9 / zstd / brotli 默认 level 9(或 zstd -19 / brotli -11)是从单文件 工具中能得到的最优压缩率。代价是 compress 时间 比 lha 慢 5 倍多,压缩率只差 5%。
最快的 compress zip -6 约 62 ms。DEFLATE64 关闭,没有 tar blocking, 每个文件少量开销。比 deflate -6 损失 6% 压缩率。
最快的 decompress(CDN、OTA、镜像) lhazip 两种工具在本语料上都在个位数毫秒级。lha 产物略大, 但解压后的代码体积本来就小;网络延迟主导。如果还关心 "无 zlib、无 libiconv 依赖",lha 才是答案。
CI 产物 + CDN 的单文件静态二进制 lha x eget use lha 把一个 152 KB 全静态二进制装到 ~/.local/bin,零依赖。 tar.gz需要 libz + libiconv + tar; zip需要 libzip + libz。lha 没这棵树。
Windows / macOS 用户开箱即用的分发 zip(或 .tar.gz zip 在 Windows + macOS 上是系统组件;tar.gz 系统直接 打开。lha 需要先装二进制(x eget use lha 一行解决,但仍然是个步骤)。
嵌入式 / 固件 / 老硬件 lha 在受限目标上解压器 < 16 KB。设计目标是 640 KB RAM 的 MS-DOS 机器。

深入阅读

为什么 lha -lh5- compress 比 tar.gz -6

LZHUF(.lh5- 背后的算法)使用 1990 年设计的 静态 Huffman 表 + 快速字符串匹配。deflate (gzip / zlib)使用 LZ77 + 动态 Huffman + 滑窗,编码器 需要在滑窗里做选择。对没填满滑窗的输入(我们的 1.8 MiB 语料放 32 KB 窗口里还有富余),deflate 大部分时间 都在做匹配扫描;LZHUF 不需要。解压侧两者 几乎打平 — 解码回路结构差不多。

在比 deflate 窗口大得多的语料上(32 KB @ -6),deflate 会因为能找更远距离的匹配而拉开压缩率优势。我们的 1.8 MiB 语料太小,-6 已经接近局部最优,compress 时间上的差距 反而更显眼。要是对 100 MiB 语料,tar.gz -9 大约会以比 lha -lh5- 多 3-5 倍的 compress 时间 拉开压缩率。

为什么 tar (无压缩) 比 input 大那么多

tar 记录每个文件的元数据(UID、GID、mode、时间戳、文件名) 加上 512 字节/记录 padding。我们 1.8 MiB / 304 个小文件 的语料里,元数据占 50% 多。tar -czf 通过 LZ77 后端能 把大部分压回去,但 原始tar 对小文件语料就是 比 input 大。

为什么 zip -6 decompress 比 tar.gz -6

zip 的归档格式在文件末尾有一个 central directory — 解码器可以非流式地读它。tar.gz 必须对每条记录 扫 tar header 来找 size,然后让 deflate 解码器填 那个 exact size。zip 的 deflate 可以 wrap 整个 central directory; tar.gz 的 deflate 是 per-record 的,每条记录要单独解析 header。差距在文件数变多时拉大。

为什么 lha 的 decompress 比 deflate / zstd / lz4 慢?

Decompress 是 LZHUF(.lh5-.lh7- 背后的算法)显出年代感的地方。1990 年的设计是 位串行(bit-serial)的:

所以这个问题的答案是:算法本身就是天花板,不是 实现。LZHUF 1990 年针对"286 + 1 MB RAM 上快速解压" 做调优,位串行解码回路就是天然契合。给 lha 加 SIMD 优化也许能在 decompress 上拿到 1.5-2 倍(length-distance copy 是唯一能向量化的大块),但底层的位串行 Huffman 循环 是根本串行的。要想解压更快,必须换算法,而不是对 LZHUF 做微观优化。

对比:zstd decompress 比 lha 在同等压缩率下 快约 5-10 倍,lz4 还能再快 3-5 倍。这两个 都是按现代 SIMD CPU(SSE2/AVX2 on x86_64,NEON on aarch64)调优的,decompress 回路是 SIMD 友好的。lha 比 MMX(1996)早六年,最初的维护者没有向量单元可打。

什么时候这有影响:lha 是 "系统上没装 zstd/lz4 时 一个单文件静态二进制"的正解。lha 不是需要 微秒级 decompress 的高吞吐 CDN 的正解。

怎么复现

在任何 Unix-like 主机上(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/

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

# 每工具 5 次取中位数。表中数字来自 5 次(median ± sample-stdev),
# Apple M 系列笔记本。
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

数字会因机器而异(不同 CPU 之间差约 10%,跨 macOS / Linux 差约 5%),但 compress 速度的相对排序 (zip → lha → tar → tar.gz)、decompress (tar.gz → zip → lha → tar)、压缩率 (tar.gz → lha → zip → tar) 是稳定的。