无损压缩这件事,在纯软件世界里讨论,大家张口就是压缩率、吞吐量、CPU占用。但一旦把场景挪到 FPGA 上,评价维度就完全变了——你手里没有现成的 x86 指令集,没有几十 GB 的内存带宽,也没有操作系统帮你调度。你有的是一块可编程逻辑、几块 BRAM、若干 DSP,以及一条必须自己设计的数据通路。这时候再问"GZIP、LZ4、Snappy、Zstandard 该选哪个",答案就不再是简单的跑分对比,而是"哪个算法的数据依赖特性、字典结构、状态机复杂度,能被我的 FPGA 资源吃下去,并且跑得比软件快"。
这篇文章想聊的就是这个问题。它适合正在做 FPGA 数据采集、高速接口、存储加速、网络预处理这类项目的工程师,也适合刚入门、想搞清楚"为什么 FPGA 上不能直接照搬软件压缩库"的朋友。我会把四种算法拆到"硬件友好度"这个层面来讲,结合资源、时序、吞吐、延迟几个维度,给出可落地的选型思路和实现要点。核心关键词:GZIP、LZ4、Snappy、Zstandard、FPGA 无损压缩。
1. 先搞清楚 FPGA 做压缩到底难在哪
1.1 软件压缩库的假设,在 FPGA 上几乎全不成立
软件压缩库的设计前提是:你有一条很宽的、低延迟的、随机访问的内存总线,有一个能跑分支预测的 CPU,还有几乎无限的"临时变量"空间。LZ77 系列算法在软件里靠哈希表找匹配,哈希表可以放在堆上,大小随数据量动态增长;匹配长度可以循环比较,比较次数多点无非是慢一点。这些在 CPU 上都是"用时间换空间"的常规操作。
到了 FPGA 上,情况反过来。BRAM 是有限的、固定的,你不能动态申请;逻辑是并行的,但状态机一旦复杂,时序就压不住;最关键的是,FPGA 最擅长的是"固定流水线 + 确定性延迟",最不擅长的就是"带复杂分支和随机访问的循环"。所以一个算法能不能上 FPGA,第一眼看的是它的数据依赖模式是不是规则的、能不能流水化。
1.2 压缩算法在硬件上的三个硬指标
我在实际项目里评估一个压缩算法能不能上 FPGA,通常先看三件事:
- 匹配查找的访存模式:是顺序扫描还是随机跳转?随机跳转意味着多级 BRAM 访问和复杂的地址生成,时序很难收敛。
- 状态机的深度:编码器需要维护多少历史状态?状态越少,越容易做成定长流水线。
- 解码器的自同步能力:解码能不能不依赖全局状态、按块独立进行?这直接决定了能不能并行解码、能不能做低延迟流式处理。
这三条基本能筛掉一大半"软件上很香、硬件上很痛"的方案。下面逐个算法对照着看。
1.3 一个容易被忽略的前提:你的瓶颈到底在压缩还是在搬运
很多项目一上来就说"我要在 FPGA 里做压缩",但真正测下来,瓶颈往往不在压缩算法本身,而在数据搬运和接口带宽。比如做高速 ADC 采集,前端 1 GSPS、12 bit,原始码率就是 12 Gbps,这时候你压缩省下来的带宽,可能还不如把 DDR 访问效率从 60% 提到 85% 来得实在。
所以在选算法之前,先算清楚:你的输入码率是多少、输出接口能扛多少、压缩比每提升 0.1 能省多少带宽、这些带宽值不值得多花几千个 LUT。这个账算不明白,后面选什么算法都是拍脑袋。
2. 四种算法的硬件友好度逐项拆解
2.1 GZIP/DEFLATE:压缩率不错,但硬件实现代价偏高
GZIP 底层是 DEFLATE,核心是 LZ77 加 Huffman 编码。它的压缩率在文本类数据上表现很好,通用性强,生态也最成熟。但放到 FPGA 上,DEFLATE 有两个绕不开的麻烦。
第一是 LZ77 的匹配查找。DEFLATE 用的是 32 KB 滑动窗口加哈希链,匹配时要在哈希链上回溯多个候选位置。这个"回溯"在软件里就是一个 while 循环,在硬件里就是状态机反复跳转,每次跳转都要访问窗口 BRAM,延迟不可预测。要做成高吞吐,通常得用多路并行匹配单元,资源消耗陡增。
第二是 Huffman 编码。DEFLATE 用的是动态 Huffman,码表随数据块变化,编码器要先统计频率、建树、生成码表,再编码。建树这个过程在硬件里非常别扭,涉及排序和变长码生成。很多 FPGA 的 DEFLATE 实现干脆退而求其次,用静态 Huffman 或者固定码表,牺牲一点压缩率换时序收敛。
提示:如果你的项目对压缩率极其敏感、数据又是文本类,且吞吐要求不高(比如几百 Mbps 级别),GZIP 的 FPGA 实现是可行的,但要做好资源预算翻倍的准备。高吞吐场景下我不推荐它。
2.2 LZ4:为速度而生,硬件上最"顺眼"的一个
LZ4 的设计目标就是极致的编解码速度,它的格式极其简单:一系列 token,每个 token 高 4 位表示字面量长度、低 4 位表示匹配长度,后面跟字面量和匹配偏移。没有 Huffman,没有熵编码,就是纯粹的 LZ77 变体加极简封装。
这种简单性对 FPGA 太友好了。解码器几乎可以做成纯组合逻辑加少量状态:读 token、读字面量、读匹配偏移、从窗口复制。匹配复制是顺序的,地址生成规则,非常适合流水化。LZ4 官方基准里解码能跑到几个 GB/s,在 FPGA 上做多路并行解码,吞吐还能再往上堆。
编码端稍复杂一点,因为要找匹配。但 LZ4 的匹配查找允许"够用就行"——它不追求最优匹配,找到第一个满足条件的就发。这个特性让硬件编码器可以用一个简单的哈希表加单次探测,不需要回溯,时序压力小很多。
2.3 Snappy:和 LZ4 同源思路,格式更规整
Snappy 是 Google 出的,设计目标和 LZ4 类似,都是"快"。它的格式由一系列 tag 组成,tag 的低 2 位区分字面量、拷贝等类型,长度编码用变长字节。相比 LZ4,Snappy 的格式在某些实现里更规整,解码状态机可以做得更扁平。
不过 Snappy 在 FPGA 社区的公开实现比 LZ4 少一些,参考资料没那么丰富。它的压缩率和 LZ4 基本在同一档,互有胜负,取决于数据类型。如果你团队里已经有人熟悉 Snappy 的格式,用它也没问题;如果从零开始,LZ4 的生态和可参考实现更多,上手会快一点。
2.4 Zstandard:压缩率与速度的平衡大师,但硬件门槛最高
Zstandard(zstd)是 Facebook 出的,目标是在压缩率和速度之间取得比 zlib 更好的平衡。它支持多级压缩,从极速模式到高压缩比模式跨度很大。高压缩比模式下它用了 FSE(有限状态熵)和更复杂的匹配查找,压缩率能逼近甚至超过 GZIP。
但正因为功能丰富,zstd 的硬件实现难度是四个里最高的。FSE 解码涉及状态转移表,虽然理论上可以流水化,但表的大小和状态更新的依赖链对时序是考验。它的帧格式也比 LZ4 复杂得多,解码器要处理多种块类型和参数。目前公开的 FPGA zstd 实现,大多只支持解码,且集中在特定配置下,编码端的高压缩比模式基本没人敢碰。
2.5 一张表看清四者的硬件取舍
| 算法 | 压缩率档位 | 编码硬件复杂度 | 解码硬件复杂度 | 典型 FPGA 吞吐 | 适合场景 |
|---|---|---|---|---|---|
| GZIP/DEFLATE | 高 | 高(Huffman 建树难) | 中高 | 百 Mbps~1 Gbps | 文本、低吞吐、压缩率优先 |
| LZ4 | 中 | 中(可单次探测) | 低 | 数 Gbps~十几 Gbps | 高速采集、网络、存储加速 |
| Snappy | 中 | 中 | 低 | 数 Gbps | 与 LZ4 类似,生态稍弱 |
| Zstandard | 中~高 | 极高 | 高(FSE) | 解码可达 Gbps 级 | 解码加速、压缩率与速度兼顾 |
这张表是我自己项目里总结的粗略档位,具体数字跟工艺、时钟、并行路数强相关,不要当成绝对指标,但相对关系是稳的。
3. 选型不能只看算法,得看你的数据长什么样
3.1 数据熵值决定了压缩率的天花板
同样用 LZ4,压缩一段英文日志能到 2.5:1,压缩已经压过的 JPEG 可能只有 1.02:1,压缩传感器原始采样数据可能连 1.1:1 都到不了。原因很简单:压缩算法吃的是数据里的冗余,冗余少,谁来都压不动。
所以在选算法前,我建议先做一件事:拿你真实的数据样本,在 PC 上用各算法的软件库跑一遍,看压缩率分布。如果 LZ4 只能压到 1.1:1,那 zstd 高压缩比模式可能也就 1.3:1,为了这 0.2 的差距去啃 FSE 硬件实现,性价比极低。反过来,如果数据是结构化文本、LZ4 能到 2:1 以上,那硬件实现 LZ4 就很值。
3.2 数据块大小与窗口的匹配
LZ4 和 Snappy 都支持块压缩,块大小可以配置。块越大,匹配窗口越大,压缩率越高,但硬件里需要的窗口 BRAM 也越大。一个 64 KB 的窗口,按字节存就是 64 KB BRAM,这在中小规模 FPGA 上已经是不小的开销。
我的经验是:先确定你能拿出多少 BRAM 做窗口,反推最大块大小,再看这个块大小下压缩率够不够。如果 BRAM 紧张,可以考虑用分布式 RAM 或者把窗口放在外部 DDR 里,但后者会引入访问延迟,影响吞吐。
3.3 流式还是块式,决定了架构走向
流式压缩(数据边来边压)对延迟敏感,要求编码器能持续吞数据,不能停下来等一整块。块式压缩可以攒够一块再处理,架构上更自由,但引入块级延迟。
FPGA 项目里,如果前端是连续采样的 ADC 或者线速网络,基本得走流式,这时候 LZ4 的优势就非常明显——它的编码器可以做成定长流水线,每个周期吞固定字节数。zstd 和 GZIP 的流式编码器在硬件里做起来要痛苦得多。
4. LZ4 编码器的 FPGA 实现要点
4.1 匹配查找:单次探测换时序收敛
LZ4 编码器的核心是找匹配。软件实现通常用一个哈希表,把当前位置的 4 字节哈希到表项,表项存最近出现的位置,然后比较。硬件里我一般这样做:
- 维护一个哈希表,表项存"最近一次出现该哈希值的位置",表大小取 2 的幂,比如 4096 或 16384 项。
- 每个周期对当前 4 字节算哈希,读表,拿到候选位置,和当前位置比较。
- 如果匹配长度达到阈值(LZ4 最小匹配是 4 字节),就输出匹配;否则输出字面量,并更新哈希表。
关键是只做单次探测,不回溯。这会损失一点压缩率(可能 3%~8%),但换来的是确定性的单周期查找,时序非常干净。实测下来,对于大多数数据,这点损失完全可接受。
4.2 哈希函数的选择:要快、要散
哈希函数在硬件里要满足两个条件:组合逻辑深度浅、分布均匀。我常用的是"乘法加移位":取 4 字节,乘以一个常数,取高位若干位作为索引。乘法可以用 DSP 做,一个周期出结果。常数一般选黄金比例相关的值,比如 2654435761,这个在软件哈希里很常见,硬件里同样好用。
注意:哈希表项数不要盲目求大。表越大,BRAM 占用越多,而且大表在综合时可能被拆成多块,反而影响时序。4096 到 16384 项对多数场景够用了。
4.3 字面量与匹配的输出打包
LZ4 的 token 结构要求把字面量长度和匹配长度打包进一个字节。硬件里要处理长度超过 15 的情况,需要额外的长度字节。这部分逻辑不复杂,但要小心边界:字面量长度为 0、匹配长度为最小值的各种组合都要覆盖到,否则解码端会出错。
我一般会写一个专门的打包状态机,把"输出 token、输出扩展长度字节、输出字面量、输出匹配偏移"分成几个状态,每个状态一拍或几拍完成。这样虽然吞吐比纯流水线低一点,但逻辑清晰,验证容易。
4.4 一个实测的吞吐估算
假设时钟 200 MHz,编码器每 4 个周期处理 8 字节输入(含打包开销),那么吞吐约 200M/4*8 = 400 MB/s。如果做 4 路并行,每路独立处理一个数据流,吞吐能到 1.6 GB/s。这个量级对很多采集场景已经够用了。想再高,就得压缩打包周期数,或者提高并行度。
5. LZ4 解码器:真正适合 FPGA 的部分
5.1 解码状态机可以做得非常扁平
LZ4 解码比编码简单得多,因为不需要找匹配。基本流程是:读 token,解析字面量长度,复制字面量到输出,解析匹配偏移和长度,从窗口复制匹配数据,循环。整个过程没有搜索,没有回溯,状态数很少。
我见过的最激进的实现,把解码做成几乎纯组合的流水线,每个周期输出多个字节。当然这需要窗口 BRAM 支持多端口读,资源换吞吐。
5.2 窗口复制的地址生成
匹配复制是从"当前位置减去偏移"的地方读数据,写到当前位置。这个地址生成是规则的:读地址 = 写地址 - 偏移,长度递增。硬件里用一个减法器加计数器就能搞定。唯一要注意的是当偏移小于复制长度时,会出现"重叠复制",也就是刚写进去的数据马上要被读出来。这在硬件里需要特殊处理,通常做法是让写优先于读,或者用一个小 FIFO 缓冲。
5.3 多路并行解码的可行性
LZ4 的块格式允许把一个大块切成多个独立子块,每个子块独立解码。这给并行解码提供了天然条件。你可以实例化多个解码器,每个处理一个子块,最后拼接输出。代价是每个解码器都要有自己的窗口 BRAM,资源翻倍。所以并行度不是越高越好,要算资源账。
6. 如果非要上 Zstandard,硬件上怎么下手
6.1 先只做解码,别碰编码
zstd 的编码端,尤其是高压缩比模式,硬件实现难度极大,公开资料也少。我的建议是:如果你的场景是"数据在别处压缩、在 FPGA 里解压",那可以研究 zstd 解码。如果是"FPGA 里压缩",老老实实选 LZ4。
6.2 FSE 解码的流水化思路
FSE 解码的核心是状态转移表。每个状态根据输入比特流查表,得到输出符号和新状态。这个查表可以放在 BRAM 里,状态更新是串行的,但可以通过预取和流水线隐藏延迟。难点在于比特流的读取是变长的,需要仔细设计比特缓冲。
6.3 帧格式解析的复杂度
zstd 的帧头包含魔数、帧头描述符、窗口描述符、字典 ID 等字段,块头又有多种类型。解析这些字段本身不难,但要做得健壮,需要覆盖各种边界情况。这部分逻辑量不小,会占用不少 LUT。
7. 几个实际项目里的选型决策记录
7.1 高速采集场景:LZ4 完胜
之前做过一个项目,前端是高速 ADC,持续采样,数据要实时存到固态存储。原始码率很高,存储接口扛不住,必须压缩。评估下来,数据是传感器原始采样,熵值中等,LZ4 能压到 1.8:1 左右,zstd 高压缩比模式能到 2.1:1,但硬件实现周期长、风险高。最后选 LZ4,用 4 路并行编码,吞吐满足要求,压缩率也够用,项目按期交付。
7.2 网络预处理场景:LZ4 解码做线速
另一个项目是网络数据预处理,进来的数据是 LZ4 压缩的,要在 FPGA 里解压后做解析。这个场景解码是刚需,LZ4 解码器做得非常轻量,几路并行就能跟上线速。如果用 zstd,解码器资源会大不少,时序也更难收敛。
7.3 文本日志场景:GZIP 仍有价值
也有项目数据是文本日志,压缩率优先,吞吐要求不高。这种场景 GZIP 的 FPGA 实现虽然资源多,但能接受,因为吞吐压力小。不过说实话,这种场景用软件压缩往往更划算,除非有特殊的实时性要求。
8. 踩过的坑和几条硬经验
8.1 别低估验证的工作量
压缩算法的硬件实现,功能验证只是第一步。真正花时间的是边界情况:空块、全字面量块、超长匹配、窗口回绕、比特流对齐。我建议一开始就搭一个软件参考模型,用随机数据做大规模对拍,把覆盖率跑上去。这个投入绝对值得。
8.2 时序收敛要靠架构,不是靠约束
很多人时序压不住就拼命加约束、改综合选项,这是治标不治本。压缩算法的时序问题,根源往往在架构:状态机太深、组合逻辑太长、BRAM 访问路径太多级。解决办法是拆流水线、加寄存器、把复杂判断提前算好。架构对了,时序自然收敛。
8.3 压缩率不是越高越好
这一点我反复强调。压缩率每提升一点,硬件代价可能是指数级的。在 FPGA 上,找到"够用的压缩率 + 可接受的资源 + 能收敛的时序"这个平衡点,比追求极致压缩率重要得多。LZ4 之所以在 FPGA 圈受欢迎,就是因为它在这个平衡点上站得很稳。
8.4 先跑软件基准,再动手写 RTL
动手写 RTL 之前,一定先用软件库在你的真实数据上跑一遍,拿到压缩率、吞吐、延迟的基线。这个基线是你后面所有硬件决策的依据。没有基线就开工,最后很可能做出来发现压缩率不达标,或者根本没必要做硬件。
9. 给不同阶段工程师的建议
如果你是刚入门 FPGA、想练手压缩算法,我建议从 LZ4 解码器开始。它的状态机简单,格式清晰,网上可参考的实现也多,做完能对"流式解码""窗口管理""变长编码"这些概念有直观理解。做完解码再挑战编码,难度是递进的。
如果你是有经验的工程师、正在做项目选型,我的建议是:先明确瓶颈在压缩还是在搬运,再用真实数据跑软件基准,然后按"解码优先、编码谨慎"的原则选算法。LZ4 在绝大多数 FPGA 场景下都是最稳的选择,除非你有明确的理由必须用别的。
如果你在评估 zstd,先问自己一个问题:我是要编码还是只要解码?只要解码,可以研究;要编码,尤其是高压缩比编码,做好打持久战的准备,或者干脆换方案。
最后分享一个我自己的习惯:每次做压缩相关的 FPGA 项目,我都会在 PC 上写一个小工具,把同一份数据分别用 LZ4、Snappy、zstd、GZIP 压一遍,打印压缩率和耗时,再模拟一下硬件里可能的块大小和窗口大小。这个工具花不了多少时间,但能帮我在项目早期就排除掉不合适的方案,避免后面返工。这个习惯我保持了几年,屡试不爽。