Hotdry.

Article

LZ4 解压器在 Legacy CPU 上的字节级流水线优化:无 SIMD 场景的指令级并行策略

针对无 SIMD 单元的 Legacy CPU,从 Z80、8080、8086、6502 四种架构的 LZ4 解压实现中提炼字节级流水线优化策略,涵盖分支预测、内存对齐与指令级并行的工程化参数。

2026-05-23systems

在无 SIMD 指令集支持的老旧处理器上实现 LZ4 解压,核心挑战并非算法复杂度,而是如何将 inherently branchy 的解压逻辑映射到寄存器稀缺、流水线简单的硬件架构上。近期针对 Z80、Intel 8080/8086 以及 MOS 6502 四种经典 CPU 的 LZ4 解压器实现对比研究表明,通过精细的字节级流水线设计和内存访问模式优化,即使在最受限的 8 位平台上也能获得可观的解压吞吐。

架构约束与算法适配

LZ4 压缩格式的核心操作是交替进行字面量(literals)复制和后向引用(backreference)复制。这一模式对不同架构提出了差异化的实现要求。

Z80 的优势在于其 LDIR 块复制指令,该指令能够在单个指令周期内完成从 (HL)(DE) 的字节搬运并自动递增指针。这使得 Z80 实现仅需维护源指针 HL 和目标指针 DE 两个长期存活的状态,第三个临时指针(后向引用源)通过 EX (SP),HL 指令与栈顶交换即可快速切换,无需额外的寄存器压力。

相比之下,6502 的架构限制更为严苛:其零寄存器可直接作为指针使用,所有指针操作必须通过零页内存中的间接寻址完成。这迫使 6502 实现采用完全不同的数据流组织方式 —— 将源地址和目标地址存储在零页,使用 Y 寄存器作为索引,通过 (src),Y(dest),Y 模式访问数据。这种设计消除了寄存器压力,但增加了内存访问频次。

8080 作为 Z80 的前身,缺乏 LDIR 指令,必须用显式循环替代块复制操作。更微妙的是,8080 没有移位指令,提取 token 的高 4 位需要通过四次 RRC(循环右移)加掩码操作完成,而 Z80 虽然支持 SRL A 逻辑右移,但 inherited 的 RRCA 指令在代码大小和执行周期上更优 —— 这是 Legacy CPU 优化的典型权衡:利用旧指令集的特性往往比使用新指令更高效。

8086 则展现了 16 位架构的灵活性。其字符串指令集(LODSBSTOSBMOVSBREP)不仅替代了 Z80 的 LDIR,还提供了更细粒度的控制。LODSB 等效于 MOV AL,[DS:SI]; INC SIREP MOVSB 则结合 CX 计数器实现块复制。值得注意的是,8086 实现采用了远指针(far pointer)设计以覆盖 1MB 地址空间,这要求在复制后向引用时临时交换 DSES 段寄存器。

字节级流水线优化策略

分支最小化与预测友好结构

LZ4 解压的 token 解析阶段 inherently 包含分支:token 字节的高 4 位表示字面量长度,低 4 位(加 4)表示匹配长度。优化策略是将高频路径(字面量长度 < 15 且匹配长度 < 19)置于循环前端,延迟处理需要读取额外长度字节的 rare case。

在 Z80/8080 实现中,.rdlen 辅助函数采用统一接口:输入寄存器 A 包含初始长度值(0-15),若值为 15 则进入扩展长度读取循环,否则直接返回。这种设计确保绝大多数迭代走快速路径,分支预测器(在支持预测的架构上)能够稳定命中。

6502 的实现进一步展示了无分支算术的技巧。在计算后向引用地址时,6502 代码在读取 16 位偏移量的同时执行减法:LDA dest; SBC (src),Y; STA .bksrc,将地址计算与数据读取融合,避免了显式的临时存储和后续加载。

内存对齐与访问模式

Legacy CPU 的内存访问代价高昂,优化目标是最大化顺序访问、最小化随机访问。

Z80 的 LDIR 和 8086 的 REP MOVSB 都受益于顺序内存访问模式。在 6502 实现中,.ldir 辅助函数采用嵌套循环结构处理 16 位计数器:外层循环递减高字节,内层循环(X 寄存器)递减低字节。关键优化点在于循环终止条件的处理 ——6502 没有 16 位递减指令,必须通过检查低字节是否为零来决定是否递减高字节,否则会出现计数错误(如 $0101 只循环一次而非 257 次)。

对齐策略在 8086 上尤为重要。虽然文章未明确讨论对齐要求,但 MOVSB 操作在字(word)边界对齐时性能最佳。实现中通过 DS:SIES:DI 的初始对齐设置,确保批量复制阶段的数据访问效率。

指令级并行与寄存器分配

在寄存器稀缺的 8 位平台上,寄存器分配策略直接影响性能。

Z80 实现采用固定角色分配:HL 为源指针,DE 为目标指针,BC 为计数器,A 为累加器。这种分配与 LDIR 指令的寄存器约定一致,无需数据移动即可直接调用块复制。

8086 实现利用其丰富的寄存器集(AXBXCXDXSIDI)避免了栈操作。后向引用指针计算在 DX 中进行,与 SI 交换时使用 XCHG DX,SI,比栈操作更高效。

6502 的寄存器分配最为受限:A 用于算术运算,XY 用于索引。实现中 Y 被锁定为 0 以支持 (src),Y 间接寻址,计数器使用 X 寄存器。这种分配迫使频繁使用零页内存存储指针和中间状态,但通过精心安排,内存访问模式仍保持较高的局部性。

可落地的优化参数

基于上述分析,针对无 SIMD Legacy CPU 的 LZ4 解压器实现可遵循以下工程化参数:

寄存器分配清单

  • 源指针:优先使用架构支持自动递增的寄存器对(Z80 的 HL、8086 的 SI
  • 目标指针:同样选择支持自动递增的寄存器对(Z80 的 DE、8086 的 DI
  • 计数器:使用支持递减到零并设置标志位的寄存器(Z80 的 BC、8086 的 CX
  • 临时数据:限制在 1-2 个寄存器,其余溢出到零页或栈

分支优化阈值

  • 字面量长度扩展阈值:15 字节(LZ4 规范)
  • 匹配长度扩展阈值:19 字节(15 + 4,LZ4 规范最小匹配长度)
  • 内联复制上限:4-8 字节(短匹配直接内联展开,避免循环开销)

内存访问模式

  • 优先顺序访问,避免随机指针跳转
  • 后向引用复制时,若偏移小于目标指针增量,需处理重叠(LZ4 规范保证最小匹配长度为 4,天然避免单字节重叠问题)
  • 零页(6502)或等效快速内存用于存储频繁访问的指针和计数器

指令选择原则

  • 优先使用 1 字节指令而非 2 字节扩展指令(Z80 的 RRCA vs SRL A
  • 小常数加法优先使用多次 INC 而非通用加法(8 位平台上 INC BC ×4 比 ADD 更快)
  • 块复制优先使用架构专用指令(LDIRREP MOVSB),回退到手动循环时保持 4-8 字节展开

实现权衡与移植建议

从四种架构的对比中可以提炼出跨平台移植的关键洞察:

Z80 实现作为 "基准线",其 LDIR 中心的设计思路可直接映射到 8080(手动循环替代)和 8086(字符串指令增强)。6502 实现则需要根本性重构,从 "指针驱动" 转变为 "数组索引驱动",利用其间接索引寻址模式的优势。

对于现代嵌入式开发者在 Cortex-M0 等无 SIMD 的 32 位 MCU 上实现 LZ4 解压,建议借鉴 8086 的策略:利用 32 位寄存器进行指针运算,使用 LDMIA/STMIA 批量加载 / 存储指令进行块复制,同时保持分支最小化的循环结构。

Legacy CPU 优化的核心启示在于:在指令集受限的环境下,算法实现必须与硬件特性深度耦合。通过理解目标架构的寻址模式、寄存器约定和指令周期,可以在不增加硬件复杂度的前提下显著提升解压性能。


资料来源

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com