LZ4 字典压缩与随机访问解压:dictionaryRandomAccess 示例深度解析
【免费下载链接】lz4Extremely Fast Compression algorithm项目地址: https://gitcode.com/GitHub_Trending/lz/lz4
dictionaryRandomAccess是 LZ4 官方仓库 examples/ 目录下的核心教学示例,它同时演示了两项实用技术:基于字典(Dictionary)的流式压缩,以及面向压缩块的随机访问(Random Access)解压。本文以 examples/dictionaryRandomAccess.md 为主线,结合 examples/dictionaryRandomAccess.c 的完整实现、lib/lz4.h 的流式 API 文档与 tests/test-lz4-dict.sh 的字典测试,讲解文件格式、压缩/解压流程、命令行用法与底层原理,帮助读者掌握“对同质化文件既能保持压缩比、又能任意定位读取”的自定义 LZ4 块流方案。
示例定位:它不是 lz4frame,而是一个教学用的自定义格式
先明确边界:该示例的输出文件不是lz4frame 格式,也不保证跨平台可读。原文档在开头就强调“Please note that the output file is not compatible with lz4frame and is platform dependent.”(examples/dictionaryRandomAccess.md)。这意味着:
- 它不使用 lib/lz4frame.c 提供的帧封装(magic、块头、内容大小、校验和等自描述元数据);
- 它直接以裸块(raw block)方式写入压缩数据,并自己设计了一套极简的“魔数 + 块区 + 跳表”容器布局;
- 跳表中的偏移量、块计数均以本机
int(4 字节)直接fwrite/fread,没有字节序转换,因此换平台(大端/小端)或换int宽度即不可读。
也正因如此,它非常适合用来学习LZ4 Block 级 API 与流式 API 的协作方式,而不是用于生产存储。与它同目录的 examples/streaming_api_basics.md、examples/blockStreaming_doubleBuffer.md 分别介绍了流式 API 基础与双缓冲模型,而本文示例的独特价值在于**“字典 + 独立块 + 跳表”**三者的组合。
核心设计:字典做历史、块做边界、跳表做索引
原文档明确指出本示例的两个技术点:
- Dictionary based compression for homogeneous files:对同质化文件(如日志、结构化记录、代码片段集合等)使用字典压缩;
- Random access to compressed blocks:对压缩块进行随机访问。
实现思路(examples/dictionaryRandomAccess.c 中test_compress(),L57-L105)是:从字典文件中读出 1 KiB 内容,将其作为每个块的压缩历史。压缩每个块之前调用LZ4_loadDict()把字典装载进流状态——由于LZ4_loadDict()本身会触发一次 reset(见 lib/lz4.h 中“LZ4_loadDict() triggers a reset, so any previous data will be forgotten”),因此块与块之间互不依赖,只共同依赖字典。这样既保证了任意一块都能脱离其他块独立解压(随机访问的前提),又借助字典提供了可复用的匹配历史,维持了压缩比。
原文档给出的压缩流程示意图:
Dictionary + | v +---------+ | Block#1 | +----+----+ | v {Out#1} Dictionary + | v +---------+ | Block#2 | +----+----+ | v {Out#2}可以看到,每个块都以同一份字典作为“前方的历史窗口”,彼此互不相干。这与 examples/blockStreaming_doubleBuffer.md 中块之间依赖前一数据块的连续流式压缩有本质区别——后者块间有依赖、压缩比更高但无法随机访问;前者块间无依赖、可随机访问且仍保留字典带来的压缩收益。
文件格式:魔数 + 压缩块 + 跳表
整个文件布局由原文档给出:
+------+---------+ +---------+---+----------+ +----------+-----+ | TEST | Block#1 | ... | Block#N | 4 | Offset#1 | ... | Offset#N | N+1 | +------+---------+ +---------+---+----------+ +----------+-----+各字段含义如下(结合源码逐项确认):
| 字段 | 内容 | 说明 |
|---|---|---|
TEST | 4 字节魔数 | 源码中定义为const char kTestMagic[] = { 'T', 'E', 'S', 'T' }(examples/dictionaryRandomAccess.c) |
Block#1 .. Block#N | N 个压缩块 | 每块对应BLOCK_BYTES(1024)字节未压缩数据,最后一块可能不足 1024 字节 |
Offset#1 .. Offset#N(共 N+1 个) | 跳表 | 每个 4 字节整数,记录“写完某个块之后”累计写入的总字节数(含魔数) |
N+1(最后 4 字节) | 块数 | 存储的是“偏移量个数”,即块数 N + 1 |
关于跳表,原文档的表述是:“If there are N blocks, then just before the last 4 bytes is N+1 4 byte integers containing the offsets at the beginning and end of each block. Let Offset#K be the total number of bytes written after writing out Block#Kincludingthe magic bytes for simplicity.” 结合源码(examples/dictionaryRandomAccess.c)可进一步明确偏移数组的生成规则:
offsets[0] = sizeof(kTestMagic),即 4——写完整块魔数后、写第一个块之前的位置;- 每写完第 k 个块:
offsets[k] = offsets[k-1] + cmpBytes; - 因此第 k 个压缩块的大小可由
offsets[k+1] - offsets[k]直接算出; - 文件尾部依次写入
offsets[0..N]共 N+1 个整数,再写入计数N+1。
解压端正是利用这一性质来定位块:numOffsets(读到的最后 4 字节)等于 N+1,向前跳过4 × (numOffsets+1)字节即可定位跳表起点(examples/dictionaryRandomAccess.c)。
关于魔数,源码注释坦诚指出:“This is not a great magic number because it is a common word in ASCII. However, it is important to have some versioning system in your format.”——即示例刻意选择TEST以强调自定义格式必须包含版本标识这一工程要点,读者在生产设计中应选用更不易误判的魔数。
压缩端实现:每块重载字典,逐块产出独立块
test_compress()(examples/dictionaryRandomAccess.c)的完整流程:
- 在栈上声明
LZ4_stream_t,并用LZ4_initStream()初始化(LZ4_initStream()是 v1.9.0+ 提供的栈上流上下文初始化入口,见 lib/lz4.h); - 写入魔数
TEST,并把offsets[0]置为 4; - 循环读取输入文件,每次最多
BLOCK_BYTES(1024)字节;读到 0 字节即结束; - 每次循环先调用
LZ4_loadDict(lz4Stream, dict, dictSize)重载字典(这会重置流状态,从而切断与前一块数据的依赖,见 lib/lz4.h); - 用
LZ4_compress_fast_continue()压缩当前块,输出缓冲为LZ4_COMPRESSBOUND(BLOCK_BYTES)大小的栈数组,acceleration 参数取 1(examples/dictionaryRandomAccess.c); - 把压缩块写入输出文件,并更新
offsets数组; - 全部块写完后再写跳表与块数。
LZ4_COMPRESSBOUND的定义在 lib/lz4.h:
#define LZ4_COMPRESSBOUND(isize) ((unsigned)(isize) > (unsigned)LZ4_MAX_INPUT_SIZE ? 0 : (isize) + ((isize)/255) + 16)对BLOCK_BYTES = 1024,上界为1024 + 1024/255 + 16 = 1044字节,LZ4_compress_fast_continue()在此容量下保证压缩必然成功(见 lib/lz4.h 中 “If dstCapacity >= LZ4_compressBound(srcSize), compression is guaranteed to succeed”)。
LZ4_compress_fast_continue()的块语义在这里尤其关键(lib/lz4.h):“Each invocation to LZ4_compress_fast_continue() generates a new block. Each block has precise boundaries. Each block must be decompressed separately.” 正是这种“一次调用一个独立块”的边界模型,配合每次调用前重载字典,才让示例的随机访问成为可能。需要说明的是,LZ4_compress_fast_continue()在一般流式用法中要求“前 64KB 源数据保持可用”(Note 2),而本例通过每次LZ4_loadDict()重置状态,绕过了对前一数据块的依赖。
两个防御性检查值得留意:压缩失败(cmpBytes <= 0)时以退出码 1 终止;块数超过MAX_BLOCKS(1024)时以退出码 2 终止。由于每块 1 KiB,该示例整体能处理的输入上限约为 1 MiB——这是示例为简单实现而设置的硬限制(examples/dictionaryRandomAccess.c 中注释 “For simplicity of implementation”)。
解压端实现:只解压目标区间跨越的块
test_decompress()(examples/dictionaryRandomAccess.c)演示随机访问的全部步骤,与原文档“How the decompression works”一节一一对应:
- 参数换算:
offset、length是相对未压缩输入文件的字节区间。currentBlock = offset / BLOCK_BYTES,endBlock = ((offset + length - 1) / BLOCK_BYTES) + 1,即目标区间跨越的所有块;length == 0时直接返回(L116-L123); - 校验魔数:读 4 字节并与
kTestMagic比对,不一致退出码 2(L126-L131); - 读取跳表:
seek到文件末尾-4处读出numOffsets;若numOffsets <= endBlock(跳表不完整/文件损坏)退出码 3;再seek到-4*(numOffsets+1)处读入前endBlock+1个偏移量(L134-L145); - 定位首块:
seek到offsets[currentBlock],并将offset折算为块内偏移offset % BLOCK_BYTES(L147-L148); - 逐块解压并裁剪:对每个目标块,
cmpBytes = offsets[currentBlock+1] - offsets[currentBlock]得出压缩块大小,读出后调用LZ4_setStreamDecode()装载字典、LZ4_decompress_safe_continue()解压进 1024 字节的decBuf,再用MIN(length, decBytes - offset)裁剪出需要的部分写入输出,随后offset = 0、length -= blockLength,进入下一块(L151-L174)。
解压端的状态管理值得注意:LZ4_streamDecode_t在栈上声明,解压每个块前都调用LZ4_setStreamDecode(lz4StreamDecode, dict, dictSize)重设字典(返回 1 表示成功,见 lib/lz4.h)。LZ4_decompress_safe_continue()一次只接受一个完整的块,其返回值是实际解压字节数,负数表示输入损坏或目标缓冲不足(lib/lz4.h);示例中decBuf容量恰为BLOCK_BYTES,与压缩端块大小严格对应,这也是“同步模式”流式解码成立的前提。
从算法角度看,endBlock只读取跨越目标区间的块,因此随机访问的代价正比于目标区间的大小而非整个文件的大小——这正是“随机访问”二字的含义:它不需要解压第 1 块到第 N 块的全部数据。
主程序与命令行用法
main()(examples/dictionaryRandomAccess.c)要求 4 个命令行参数:
Usage: <program> input dictionary offset length各参数含义:
| 参数 | 含义 | 示例值(来自 examples/Makefile) |
|---|---|---|
input | 待压缩的原始文件路径 | Makefile/.gitignore |
dictionary | 字典文件路径(程序只读取前DICTIONARY_BYTES=1024 字节) | 同上(用自身做字典) |
offset | 希望读取的字节区间起点(相对未压缩输入) | 1100/0 |
length | 希望读取的字节区间长度 | 1400/32 |
程序依次执行三个阶段:
- 压缩:
input → input.lz4s-1024(后缀中的1024是BLOCK_BYTES的取值,文件名命名见 examples/dictionaryRandomAccess.c); - 解压:
input.lz4s-1024 → input.lz4s-1024.dec,只产出[offset, offset+length)区间对应的数据; - 校验:将原始文件从
offset处 seek 后与.dec文件逐块memcmp比对(compare(),examples/dictionaryRandomAccess.c),一致输出verify : OK,否则输出verify : NG。
运行示例(仓库根目录下构建后执行):
cd examples && make ./dictionaryRandomAccess Makefile Makefile 1100 1400 ./dictionaryRandomAccess .gitignore .gitignore 0 32其中第一条会压缩Makefile,随后只解压第 1100 到 2499 字节的区间并与原文件校验;第二条则验证从文件头开始的 32 字节。这两条命令也被 examples/Makefile 的make test目标自动执行,属于官方回归验证的一部分。
为什么可行:LZ4 字典机制的底层原理
要理解本示例为何成立,需要知道 LZ4 块格式的压缩模型。根据 doc/lz4_Block_format.md,LZ4 是 LZ77 型、面向字节的固定编码格式,压缩核心是“在过去 64KB 窗口内检测重复数据”,匹配通过offset(2 字节、小端)与matchlength编码。它没有熵编码后端,也没有帧层——帧层由外部系统负责,这正是本示例自行设计容器布局的依据。
在 lib/lz4.h 中,LZ4_loadDict()的文档进一步说明了字典机制的关键约束:
- “The same dictionary will have to be loaded on decompression side for successful decoding.”:压缩与解压必须使用同一份字典,字典在压缩/解压期间必须保持可访问且不被修改;
- “Loading a size of 0 is allowed, and is the same as reset.”:加载 0 字节等同于重置;
- “note: only the last 64 KB are loaded”:无论传入多大字典,只有最后 64KB 会被装载进历史窗口——这与 LZ4 的 64KB 匹配窗口(doc/lz4_Block_format.md 中 offset 最大 65535)一致。本示例字典固定为 1 KiB,远小于窗口上限,因此 1024 字节全部生效。
这一“只用最后 64KB”的语义在仓库测试中也有验证:tests/test-lz4-dict.sh 遍历了 0、1、4、128、32767、65536、131073 等多种字典大小,用不同大小的字典压缩同一数据,均能正确解压,且测试同时断言使用字典的压缩结果小于不使用字典的结果(“Test Passed: dictionary is effective.”),证明字典在小数据压缩中的收益是真实可测的。
回到本示例:LZ4_loadDict()每次调用都触发 reset 并装载同一份字典,因此每块在逻辑上等价于“以字典为前置历史的独立块”。解压端LZ4_setStreamDecode()以同样方式装载字典,LZ4_decompress_safe_continue()就能在字典窗口内解析出全部匹配引用。字典同时扮演了“共同的历史前缀”,既维系压缩比,又让块之间彻底解耦——这就是随机访问得以实现的全部秘密。
限制与改进方向
作为教学示例,dictionaryRandomAccess存在若干明确限制,理解这些限制有助于在生产场景中设计替代方案:
- 格式非标准、平台相关:
int直接落盘、无字节序转换、无校验和,跨平台/跨进程不可移植;生产环境应优先使用 lib/lz4frame.h 的帧格式,或自行加入字节序规范化、版本字段与校验; - 规模上限:
BLOCK_BYTES、DICTIONARY_BYTES、MAX_BLOCKS均为编译期常量(1 KiB / 1 KiB / 1024 块),最大覆盖约 1 MiB 输入;块大小固定意味着无法按内容自适应切分; - 字典依赖外部文件:解压必须拿到与压缩时完全一致的字典文件(含相同前缀),丢失字典则全部数据不可恢复;
- 压缩比受限于字典:字典仅 1 KiB,若数据中存在字典覆盖不到的重复模式,压缩比会打折扣。文档级建议(lib/lz4.h)指出,当对字典效率拿不准时,可考虑使用 Zstandard 的 Dictionary Builder 生成更优字典。
若要在生产环境中落地“字典 + 随机访问”,可将示例中的三要素(独立块、64KB 窗口语义、跳表索引)与 lz4frame 的块元数据、CRC 校验、端序转换结合,并按需引入变长块与更大的偏移表。
延伸阅读
- examples/dictionaryRandomAccess.md:本文对应的原始文档(压缩/解压流程与文件布局图);
- examples/dictionaryRandomAccess.c:完整可编译实现;
- examples/streaming_api_basics.md:LZ4 流式 API 基础,理解
continue系列函数的前提; - examples/blockStreaming_doubleBuffer.md 与 examples/blockStreaming_lineByLine.md:块间有依赖的连续流式方案对比;
- lib/lz4.h:
LZ4_loadDict、LZ4_compress_fast_continue、LZ4_setStreamDecode、LZ4_decompress_safe_continue、LZ4_COMPRESSBOUND的权威说明; - doc/lz4_Block_format.md:LZ4 块格式规范(token、字面量、offset、matchlength 与 64KB 窗口);
- tests/test-lz4-dict.sh:CLI 层字典压缩的有效性与 64KB 尾部语义回归测试;
- examples/Makefile:
make构建与make test中该示例的自动化调用。
【免费下载链接】lz4Extremely Fast Compression algorithm项目地址: https://gitcode.com/GitHub_Trending/lz/lz4
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考