LZ4 字典压缩与随机访问解压:dictionaryRandomAccess 示例深度解析
2026/9/15 12:41:16 网站建设 项目流程

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 | +------+---------+ +---------+---+----------+ +----------+-----+

各字段含义如下(结合源码逐项确认):

字段内容说明
TEST4 字节魔数源码中定义为const char kTestMagic[] = { 'T', 'E', 'S', 'T' }(examples/dictionaryRandomAccess.c)
Block#1 .. Block#NN 个压缩块每块对应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)的完整流程:

  1. 在栈上声明LZ4_stream_t,并用LZ4_initStream()初始化(LZ4_initStream()是 v1.9.0+ 提供的栈上流上下文初始化入口,见 lib/lz4.h);
  2. 写入魔数TEST,并把offsets[0]置为 4;
  3. 循环读取输入文件,每次最多BLOCK_BYTES(1024)字节;读到 0 字节即结束;
  4. 每次循环先调用LZ4_loadDict(lz4Stream, dict, dictSize)重载字典(这会重置流状态,从而切断与前一块数据的依赖,见 lib/lz4.h);
  5. LZ4_compress_fast_continue()压缩当前块,输出缓冲为LZ4_COMPRESSBOUND(BLOCK_BYTES)大小的栈数组,acceleration 参数取 1(examples/dictionaryRandomAccess.c);
  6. 把压缩块写入输出文件,并更新offsets数组;
  7. 全部块写完后再写跳表与块数。

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”一节一一对应:

  1. 参数换算offsetlength是相对未压缩输入文件的字节区间。currentBlock = offset / BLOCK_BYTESendBlock = ((offset + length - 1) / BLOCK_BYTES) + 1,即目标区间跨越的所有块;length == 0时直接返回(L116-L123);
  2. 校验魔数:读 4 字节并与kTestMagic比对,不一致退出码 2(L126-L131);
  3. 读取跳表seek到文件末尾-4处读出numOffsets;若numOffsets <= endBlock(跳表不完整/文件损坏)退出码 3;再seek-4*(numOffsets+1)处读入前endBlock+1个偏移量(L134-L145);
  4. 定位首块seekoffsets[currentBlock],并将offset折算为块内偏移offset % BLOCK_BYTES(L147-L148);
  5. 逐块解压并裁剪:对每个目标块,cmpBytes = offsets[currentBlock+1] - offsets[currentBlock]得出压缩块大小,读出后调用LZ4_setStreamDecode()装载字典、LZ4_decompress_safe_continue()解压进 1024 字节的decBuf,再用MIN(length, decBytes - offset)裁剪出需要的部分写入输出,随后offset = 0length -= 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

程序依次执行三个阶段:

  1. 压缩input → input.lz4s-1024(后缀中的1024BLOCK_BYTES的取值,文件名命名见 examples/dictionaryRandomAccess.c);
  2. 解压input.lz4s-1024 → input.lz4s-1024.dec,只产出[offset, offset+length)区间对应的数据;
  3. 校验:将原始文件从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存在若干明确限制,理解这些限制有助于在生产场景中设计替代方案:

  1. 格式非标准、平台相关int直接落盘、无字节序转换、无校验和,跨平台/跨进程不可移植;生产环境应优先使用 lib/lz4frame.h 的帧格式,或自行加入字节序规范化、版本字段与校验;
  2. 规模上限BLOCK_BYTESDICTIONARY_BYTESMAX_BLOCKS均为编译期常量(1 KiB / 1 KiB / 1024 块),最大覆盖约 1 MiB 输入;块大小固定意味着无法按内容自适应切分;
  3. 字典依赖外部文件:解压必须拿到与压缩时完全一致的字典文件(含相同前缀),丢失字典则全部数据不可恢复;
  4. 压缩比受限于字典:字典仅 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_loadDictLZ4_compress_fast_continueLZ4_setStreamDecodeLZ4_decompress_safe_continueLZ4_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询