FAQ
性能、格式、未来格式,以及 lha-rs 问题。 这是一份对静态二进制分发背后分析的汇总记录。
为什么 lha 的解压比 deflate / zstd / lz4 慢这么多?
简短回答:算法本质是位串行的,源码里完全没有 SIMD。 通过非 SIMD 的算法优化可以获得 3-7× 加速;要追上 zstd -1 是多年的工程。
源码里的两条热点路径
解码 LZHUF 意味着:
- 读 N 位 → 走一棵二叉树(每条分支消耗一位)→
输出一个符号。(
src/huf.c:decode_c_dyn、src/huf.c:decode_p_dyn) - 如果符号是反向引用,从滑窗 D 字节之前复制 L 字节。
(
src/slide.c手写的for循环, 一字节一迭代。)
为什么这从根本上就慢
- 静态 Huffman 意味着位流是一位一位解码的。 解码器无法在不查表的情况下"预看"超过 1-2 位。
- 完全没有 SIMD:
grep -nE '(mmx|sse|avx|simd|__m128i|__builtin_ia32)' src/*.c返回零命中。vendored 1.14i 代码是 1995-2003 年纯 ANSI C, 无向量化。 - 反向引用复制是逐字节 写在
src/slide.c。用memcpy(dst, src, len)在现代 glibc(内部 用 SSE2/AVX2)上已经快 2-4 倍。
不改磁盘格式就能做的优化
| 优化 | 改在哪里 | 解压收益 |
|---|---|---|
| 预计算的符号查找表(zlib-style inflate) | 替换 src/huf.c 里的逐位树走 |
3-5×(位循环部分) |
memcpy(dst, src, len) 用于 LZ77 反向引用复制 |
替换 src/slide.c 的逐字节循环 |
2-4×(复制部分) |
| 去分支的 16 位表查找 | 同 #1,更激进 | 5-7× |
| 两阶段流水线(读 buffer + 分块解码) | 重接 src/dhuf.c 和 src/slide.c |
1.5-2× |
| 缓存热反向引用距离(top-3 MRU) | 新加在 src/slide.c |
1.2-1.5× |
合计:3-7× 解压加速,把 lha -lh5- 从 ~45 MB/s 提到
~150-300 MB/s(kenlm 语料)——和 deflate
level 1-3(同语料约 300-500 MB/s)有竞争力,但
"同类最强"还差得远。
硬上限:tANS 能买的和不买的
要追到 zstd -1(~2 GB/s),需要根本不同的熵编码器 (tANS/FSE)和更快的 LZ77。也就是改磁盘格式——见下面 "新格式放哪里?"。 今天的 lha -lh5- C 解码器是上限;LZHUF 内部没有任何 算法能突破 ~300 MB/s 解压。
来源
数据来自 5 次跑、kenlm 上游源码(1.83 MiB / 304 文件), Apple M 系列,原生 lha 编译。完整表格见 性能页。5 维源码审计见 安全页,确认位循环和逐字节 复制是仅有的两条解压热点路径。
tANS 能让 lha 和现代压缩器有竞争力吗?
简短回答:能。配上 tANS,lha 进入和
deflate / brotli 同一档;
但到不了 zstd -1+ 的档次(多年优化差距),
也到不了 lz4 / snappy 的档次
(根本是不同架构)。
tANS 在每个对手面前的定位
| 对手 | 解压速度 | tANS 为什么有 / 没有用 |
|---|---|---|
| zstd -1 | ~2 GB/s | Yann Collet 在 FSE/tANS 上调优 8+ 年。"第一版" tANS 实现能到 ~1-1.5 GB/s,比 zstd -1 慢 30-50%。 多年迭代才能追上。 |
| zstd -3+ | ~1 GB/s | 差距更大;optimal parser 是难点。 |
| deflate -1 ~ -3 | 500-800 MB/s | 同一档。tANS 能到 ~500-800 MB/s,deflate 也能。差距在小的常数因子内。 |
| brotli -1 | 300-500 MB/s | 略领先——brotli 用静态 Huffman,新 tANS 实现能快一点点(同样压缩率下)。 |
| lz4 / snappy / LZO | 3-5 GB/s | 永远追不上。这些没有熵编码器——它们把 LZ77 匹配存成 4 字节 token,解压 ~1 byte/cycle。 LHA-with-tANS 多了 lz4 没有的那层,永远追不到 那个速度档位(架构差距)。 |
硬上限
解码器被内存带宽 + 分支预测失误 + 缓存未命中三者 限制。tANS 把分支预测失误降到最低(每个符号一次分支)。 lz4 把缓存未命中降到最低(16 字节一块的 literal 复制)。 现代 CPU 跑最简单的解码器能到 ~1 byte/cycle;tANS 大约 1-2;lz4 大约 1。两种架构交换这两个瓶颈, 彼此从外部都压不死对方。
LHA 的实际竞争定位
加 tANS 给 lha 的是"和 deflate / brotli 同档",不是
"打败 zstd -1+",更不是"打败 lz4"。LHA 在 2026 年
的真正价值不是速度——而是
35 年格式稳定性 + 单文件零依赖静态二进制。
速度是那个承诺的代价。问题变成:我们是加 tANS 作为
新格式.lh8- 给追求速度的用户,
同时让 .lh5- 作为只读稳定兼容目标?见
下一节。
zstd -1 是"最佳" zstd 级别吗?
简短回答:对,zstd -1 是 zstd 里速度最快的一档
(压缩率最低、速度最高)。但"最佳默认"是 -3,
不是 -1。
zstd 级别表
| 级别 | 角色 | 压缩率 vs -3 | 编码速度 vs -3 |
|---|---|---|---|
| -1 | 最快 | ~5-10 % 差 | ~2-3× 快 |
| -3 | 默认——速度/压缩率甜点 | 1.0× | 1.0× |
| -6 | 平衡 | ~5-8 % 好 | 略慢 |
| -10 | 偏好压缩 | ~10-15 % 好 | 3-5× 慢 |
| -15 | 高压缩 | ~20-25 % 好 | 8-10× 慢 |
| -22 | 最大 | ~30-50 % 好 | 30-50× 慢 |
为什么 -3 才是默认
速度/压缩率曲线不是线性的:
- -1 到 -3:时间轴上很平,压缩率有真收益。升到 -3 是 no-brainer。
- -3 到 -10:大致线性交换。
- -10 到 -22:压缩率轴上很平,时间成本继续大。 压缩率收益递减。
所以 zstd 作者(Yann Collet)选 -3 作默认:它在曲线的 肩上。-1 大多数场景下压缩率损失太大;-10 花时间多但 收益少。
zstd -1 不是"全场最快解码器"
zstd -1 是最快zstd,但:
| 压缩器 | 解压速度 | 有熵编码? |
|---|---|---|
| lz4 | ~3-4 GB/s | 无 |
| snappy | ~3-5 GB/s | 无 |
| zstd -1 | ~1.5-2 GB/s | 是(FSE / tANS) |
| zstd -3 | ~1 GB/s | 是 |
| brotli -1 | ~300-500 MB/s | 是(静态 Huffman) |
| deflate -1 | ~500-800 MB/s | 是(动态 Huffman) |
| lha -lh5-(当前) | ~45 MB/s | 是(静态 Huffman) |
lz4 / snappy 比 zstd -1 还快 1.5-2×,因为 它们根本没有熵编码器。所以"最快解码器"是 双轨赛跑;lz4 赢无熵轨;zstd -1 赢有熵轨。
LHA 相对 bzip2 / zip / 7z 的独有特性是什么?
简短回答:lha 的"特性"大多数不是特性—— 是换 35 年稳定格式的交换。但 lha 有几个真独有的: 每文件独立压缩、stdio 流友好、可流式构建目录。
LHA 没有的
| 特性 | bzip2 | zip | 7z | lha |
|---|---|---|---|---|
| 块内随机访问(seek 到 100 KB 解 1 KB) | ✅ 100-900 KB 块 | ⚠️ 仅按文件跳 | ✅ 块 | ❌(流式) |
| 全局索引 | ❌ | ✅ 中央目录在末尾 | ❌ | ❌(但可流式构建) |
| 稀疏文件 / 硬链接 / ACL / xattr | ❌ | 部分 | ✅ | ❌ |
| 强校验和(SHA-256) | ❌(CRC32) | ❌ | ✅ | ❌(CRC32) |
| 压缩字典 / pretrain | ✅ | ❌ | ✅ | ❌ |
LHA 是流式(不是块)归档器——和 zip / gzip / tar 同一 形态,不是 bzip2 / 7z 那一档。所以 bzip2 的"seek 到 100 MB 处只解 1 KB"在 lha -lh5- 上结构上就不可能。
LHA 有的(不寻常的)
- 35 年格式稳定性。1990 年写的文件今天能解。 zip 36+ 年、tar 45+、bzip2 28、7z 25。lha 不是最长但在前档。
- 可流式构建目录。LHA header 是固定大小(每文件 ~35 B)。可以一次流式读 lha 归档,内存里建 "文件 N 在偏移 O、大小 S、mtime M"的索引,不解压任何 文件。zip 做不到(中央目录在末尾)。
- 每文件独立压缩。每条 entry 的压缩方法在它自己的 header 里。文件 1 可以是 -lh0-(存),文件 2 是 -lh5-,文件 3 又 -lh0-。没有"所有文件共享一个字典" 的硬约束。
- stdio 原生。
lha c dir/ > archive.lzh和lha xq archive.lzh < /dev/tty直接 跑。zip / 7z / tar 都需要临时文件输出到 stdout。 - 极小的解压代码。LZHUF 解码器 ~1,500 行 C——比 zstd 的 ~50,000 或 7z 的 ~30,000 小得多。易嵌入、易 审计(5 维刚刚做完)。
- 流里没有熵表。静态 Huffman 意味着表在解码器里、 不在数据里。50 字节的文件在 lha 里只有 35 字节 header + 50 字节 body,没有每块 overhead。deflate 每 块有 ~20 字节表。
- 日文文件名编码(Shift-JIS、EUC-JP)原生支持 1990 年代日本 BBS / 2ch 老归档。
战略意义
LHA 在 2026 年的价值是格式稳定性 + 零依赖 + 小体积 +
35 年可移植,不是"功能丰富"。要 bzip2 风格的功能
(随机访问、索引、强校验)就加新格式
.lh5b-(块)、.lh8-(tANS)、
.lhi-(索引)——不是 Rust 重写 LHA。详见
下个问题。
Rust 重写能帮 LHA 吗?(lha-rs 问题)
简短回答:不能。lha-rs 不值得作为 1:1 替换 C 版的 Rust 移植。诚实的答案:Rust 重写的标准理由对 lha 基本不适用。
每个标准"用 Rust 重写"的理由,应用到 lha
| "理由" | 对 lha 的现实 |
|---|---|
| 内存安全(Rust 消除 strcpy/sprintf 风险) | 5 维审计刚做完。8 个 strcpy 全部带长度检查; 0 个 sprintf / gets / scanf。脆弱类不存在。整体重写 来去掉一个不在的漏洞类别,成本/收益差。 |
| cargo-fuzz / cargo test 基础设施 | libFuzzer 不只是 Rust 用,给 C / C++ 项目也能挂。 oss-fuzz 给 C 项目免费 hosting。给 lha 加 libFuzzer 是 1-2 周工作,不是单独开项目。 |
| 现代错误处理(Result<T, E>) | C 用 tagged-union 一样能返回。真正的活是错误词汇, 不是语言。1 周改 lha 的错误路径,不是 1 年。 |
| 现代 CLI(clap / argh) | lha 的 CLI 1990 年就稳定。重构是破坏性变更,不是 改进。CLI 不是 lha 的痛点。 |
| 可插拔压缩 crate(flate2、zstd-rs、lz4_flex) | 那是给新项目用的。不需要在 lha-rs 里——任何写新 Rust 工具的人已经在用了。 |
| 算法优化载体 | 层不对。预计算表 + memcpy 两招是语言无关的。 C 里写——进现有 CI 矩阵,不需要新项目。 |
| 新格式实验(tANS 等) | 这是新格式的问题,不是 lha-rs 的问题。新格式
住在 lha/ 的 src/lh5b/、
src/lh8/。不是单独 repo。 |
什么时候 Rust 重写才真的对
必须满足以下之一:
- C 维护者要退了,下一代维护者只会 Rust——这是退出 策略。
- 新格式是Rust-first设计(用 async、
Send、rayon、std SIMD)。 - 特定用户需求强制(比如"必须在 WebAssembly 里跑"——Rust
的
wasm32-unknown-unknown是唯一靠谱路径)。
今天对 lha 这三条都不命中。
正确的下一步
- 留在 lha(C)——维护和审计。5 维安全审计已做; libFuzzer 集成 1-2 周;oss-fuzz 1-2 小时。完成。
- 在 C 里加新格式(作为子目录):
src/lh5b/(块格式带随机读)、src/lh8/(tANS)、src/lhi/(索引 文件)。C 实现直接进现有 CI 矩阵;没新 repo、没新 release 节奏。 - 写格式规范于
docs/lzh-format.md, 任何未来实现(Rust、Python、Go)都有单一事实源。 - 如果新格式被证明有用,然后单独开
ljh-sh/lha-spec(或类似)放多种参考实现。 不是"重写" repo——是"多实现"测试床。
lha-rs 不在这个清单上。6-12 个月重写一个 13,400 行 C 项目,安全画像已经好,用一个不增加价值的语言——这 不划算。保留 C、做小的增量、必要时用 Rust 做新格式——这就是 正确路径。
新格式(.lh5b- / .lh8- / .lhi-)放哪里?
简短回答:在现有的 ljh-sh/lha 仓库里,
不是新仓库。新格式是对 lha 家族的补充,不是
替代。
提议的新格式
| 后缀 | 后端 | 和 .lh5- 兼容 | 加了啥 |
|---|---|---|---|
.lh5-(当前) | LZHUF + 逐字节复制 | —(基准) | 35 年稳定格式;体积小;零依赖 |
.lh5b- | 同 .lh5-,但 256 KB 块 | 老 .lh5- 读端能解;新读端获块内随机读 | 单文件内随机访问;部分解(接近 bzip2 行为) |
.lh8- | tANS(FSE)+ 更快的 LZ77 | 新格式,老 .lh5- 解码器读不了 | 解压速度和 deflate -1 同一档;多数语料上压缩率 优于 .lh5- |
.lhi- | 索引文件(独立于归档) | 和任何 .lh5-(或 .lh8-)归档配合 | 可流式构建的目录;让
lhacat archive.lzh 列出所有文件而
不读任何 body |
每个放哪里
src/lh5b/— 块格式。C。ljh-sh/lha的子目录。4-6 周。src/lh8/— tANS 后端。C 或 Rust (tANS 是新格式,所以如果Rust 是对的选择, 这里是对的地方)。12-18 月。很可能仍用 C 保持一致。src/lhi/— 索引格式。C。1-2 周。docs/lzh-format.md— 新格式都引用的 格式规范。2-3 周。
一个二进制处理所有这些
在现有 repo 做的杀手特性:单个 ljh-sh/lha
二进制读所有这些格式:
$ lha l archive.lh5- # 基准
$ lha l archive.lh5b # 块内随机读
$ lha l archive.lh8- # tANS,更快解压
$ lha l --index archive.lhi # 流式目录
$ lha x archive.lh8- # 写新 tANS 归档
单静态二进制,~250 KB,零依赖。同一
x i lha / x eget use lha
安装路径。35 年稳定读路径 + 现代 tANS 写路径 + 块内
随机读。这是 lha 在 2026 年的竞争定位:
一个做没有任何其他归档器能做到的事情的二进制,
重量 250 KB。