☰
7-Zip-zstd 多线程压缩容器:zstdmt 库的 Skippable Frame 帧格式与多线程实现解析
2026/9/28 2:51:15 网站建设 项目流程
  • 桌面应用
  • CLI

【免费下载链接】7-Zip-zstd

7-Zip with support for Brotli, Fast-LZMA2, Lizard, LZ4, LZ5 and Zstandard

项目地址:https://gitcode.com/gh_mirrors/7z/7-Zip-zstd
点击查看免费下载

本篇文章以 C/zstdmt/README.md 为核心骨架,系统讲解 7-Zip-zstd 项目中为 Brotli、Lizard、LZ4、LZ5 与 Zstandard 五种压缩算法提供的多线程(MT)封装库 zstdmt:其基于 Zstandard Skippable Frame(0x184D2A50)的容器化帧格式、Brotli 特有的 16 字节扩展帧头、库的公开 API 与读/写回调约定,以及仓库源码中的多线程工作模型与 7-Zip 集成方式。读完本文,你将掌握该 MT 容器在字节层面上的精确布局、各算法的线程数与压缩级别取值范围,并能基于 C/zstdmt 下的头文件独立接入这套多线程压缩/解压接口。

一、zstdmt 是什么:五种算法的统一多线程封装

Brotli、Lizard、LZ4、LZ5 和 Zstandard 的参考实现本身大多是单线程的:压缩速度受限于单核。zstdmt 是 Tino Reichardt 编写的一层薄封装,它把每种算法的"单次压缩单元"(frame)按固定大小的输入块切分,用多个 worker 线程并行压缩,再把结果按 Zstandard 的 Skippable Frame 容器格式串联成一个合法、可被标准解码器识别为"可跳过帧"的流,从而实现多线程压缩与多线程解压。

从仓库布局看,这五种算法的 MT 封装各有一套对称的文件族(common 错误处理、compress、decompress)与对应的公共头文件:

算法公共头文件压缩实现解压实现底层算法源码
Brotlibrotli-mt.hbrotli-mt_compress.cbrotli-mt_decompress.cC/brotli
Lizardlizard-mt.hlizard-mt_compress.clizard-mt_decompress.cC/lizard
LZ4lz4-mt.hlz4-mt_compress.clz4-mt_decompress.cC/lz4
LZ5lz5-mt.hlz5-mt_compress.clz5-mt_decompress.cC/lz5
Zstandard—(使用官方 mt API)C/zstd/zstdmt_compress.c同左C/zstd

其中 Zstandard 的多线程能力直接复用其官方ZSTDMT_*API(C/zstd/zstdmt_compress.c 与 C/zstd/zstdmt_compress.h),而 zstd-mt_threading.c 为整个 zstdmt 库提供跨平台线程原语包装(详见下文第五节)。

二、通用 Skippable Frame 容器:Lizard / LZ4 / LZ5 / Zstandard 的 12 字节帧头

README 明确给出了这套 MT 容器的核心设计:所有压缩帧都包装成 Zstandard 的 Skippable Frame 格式,魔法数固定为0x184D2A50,每个压缩帧只需额外 12 字节的帧头开销。

通用的 Skippable Frame 定义如下(这是 C/zstdmt/README.md 的原表,完整继承):

大小值说明
4 字节0x184D2A50USkippable Frame 魔法数(magic)
4 字节4Skippable Frame 自身大小(即紧随其后只有 4 字节数据)
4 字节compressed size其后所跟压缩帧的大小(压缩数据长度)

也就是说,一个完整的 MT 压缩单元在字节流上是:[4B magic][4B size=4][4B compressed_size][compressed data ...],前 12 字节是容器帧头,其后紧跟某一块输入数据的压缩结果。之所以选择 Skippable Frame 语义,是因为标准的 Zstandard 解码器遇到该 magic 时会把它视为"可跳过帧"直接略过,从而让这份 MT 流在非 MT 感知的工具里也能被容忍、被跳过,不会因未知帧报错。

这一 12 字节布局在源码中有直接印证:LZ4、LZ5、Lizard、Brotli 四个 MT 头文件里都定义了同一个容器魔法数,例如:

  • lz4-mt.h:#define LZ4FMT_MAGICNUMBER 0x184D2204U与#define LZ4FMT_MAGIC_SKIPPABLE 0x184D2A50U
  • lz5-mt.h:#define LZ5FMT_MAGIC_SKIPPABLE 0x184D2A50U
  • lizard-mt.h:#define LIZARDFMT_MAGIC_SKIPPABLE 0x184D2A50U
  • brotli-mt.h:#define BROTLIMT_MAGICNUMBER 0x5242U /* BR */与#define BROTLIMT_MAGIC_SKIPPABLE 0x184D2A50U

所有 MT 变体共用0x184D2A50U作为容器标识,而各算法自己的帧类型魔法数(如 LZ4 的0x184D2204U)仍保留在压缩数据内部,供其原生解码器识别。

三、Brotli 的特有 16 字节帧头:BR 魔法与解压分配提示

Brotli 的封装与通用格式略有不同:由于 Brotli 原生流没有自描述解压后大小的机制,zstdmt 在帧头中额外携带了"解压分配提示",因此帧头从 12 字节扩展为 16 字节。README 中给出的 Brotli 帧定义如下(原表完整继承):

大小值说明
4 字节0x184D2A50USkippable Frame 魔法数(与 zstd 一致)
4 字节8Skippable Frame 大小(此处为 8 字节,而非通用格式的 4)
4 字节compressed size其后所跟压缩帧的大小(压缩数据长度)
2 字节0x5242UBrotli 魔法数,即 ASCII 字符"BR"
2 字节uncompressed size解压器分配提示:64KB × 此值

可以这样理解 Brotli 帧的字节布局:前 8 字节是 Skippable Frame 标准头(magic + size=8),后 8 字节是对该帧压缩数据的描述(compressed size +BR标志 + 分配提示)。

在 brotli-mt_compress.c 中可以看到这 16 字节帧头被逐字段写出的真实代码:第 0 字节起写入BROTLIMT_MAGIC_SKIPPABLE,第 4 字节起写入常量8,第 8 字节起写入本次压缩后的实际大小wl->out.size,第 12 字节起写入 2 字节的BROTLIMT_MAGICNUMBER(0x5242),最后第 14 字节起写入 2 字节的分配提示hintsize。其中hintsize的计算逻辑为:若当前输入块实际小于预设inputsize,则hintsize = (in.size >> 16) + 1,否则hintsize = inputsize >> 16——即向上取整到下一个 64KB 边界,保证解压端分配的缓冲区足够容纳整个帧的解压结果。

对应的校验与读取逻辑在 brotli-mt_decompress.c:解压器依次校验size字段必须等于8、第 12 字节起的 2 字节必须等于BROTLIMT_MAGICNUMBER,然后读取第 14 字节起的hintsize,通过hintsize << 16得到该帧解压缓冲区的建议大小,并依据第 8 字节的compressed size动态扩容输入缓冲(不足时realloc),实现"按帧头自描述、逐帧顺序解压"的流程。

四、库的使用方式:上下文结构、回调式 I/O 与错误处理

原 README 将库的用法指向独立的 testutils 与 lib 示例;在本仓库中,库的公开接口全部集中在 C/zstdmt 目录下的各*-mt.h头文件中。下面以 Brotli 为例(其余算法接口完全同构),说明接入这套库的完整调用范式,接口细节来自 brotli-mt.h。

4.1 压缩端 API

/* 1) 创建压缩上下文 */ BROTLIMT_CCtx *BROTLIMT_createCCtx(int threads, uint64_t unpackSize, int level, int inputsize, int lgwin); /* 2) 执行多线程压缩(错误通过返回值检查) */ size_t BROTLIMT_compressCCtx(BROTLIMT_CCtx *ctx, BROTLIMT_RdWr_t *rdwr); /* 3) 获取统计信息 */ size_t BROTLIMT_GetFramesCCtx(BROTLIMT_CCtx *ctx); /* 已生成的帧数 */ size_t BROTLIMT_GetInsizeCCtx(BROTLIMT_CCtx *ctx); /* 输入字节数 */ size_t BROTLIMT_GetOutsizeCCtx(BROTLIMT_CCtx *ctx); /* 输出字节数 */ /* 4) 释放上下文 */ void BROTLIMT_freeCCtx(BROTLIMT_CCtx *ctx);

各参数的含义与取值范围(均来自头文件注释与实现校验):

  • threads:0 .. BROTLIMT_THREAD_MAX(上限 128,定义于 brotli-mt.h)。传0表示不启用容器封装,直接单线程处理原始 brotli 流(输出裸 brotli 格式);传1..128则启用 MT 容器。超过上限会在BROTLIMT_createCCtx内被拒绝并返回0(见 brotli-mt_compress.c)。
  • level:BROTLIMT_LEVEL_MIN(0) .. BROTLIMT_LEVEL_MAX(11),对应 Brotli 压缩级别,超出范围同样拒绝创建。
  • inputsize:每个 worker 每次读取的输入块大小;传0时由库自动选择"该级别下的较优值"——具体实现为1024 * 1024 * (level ? level : 1)(见 brotli-mt_compress.c),即按级别线性放大分块,以平衡压缩率与并行粒度;传非 0 值则完全采用调用者的设置。
  • unpackSize:已知的输入总大小(用于统计/进度),未知可传0。
  • lgwin:Brotli 的滑动窗口大小参数(2 的幂指数),直接透传给BrotliEncoderCompress。

其余算法的常量范围可在对应头文件中确认:LZ4 为LZ4MT_THREAD_MAX=128、LZ4MT_LEVEL_MIN=1、LZ4MT_LEVEL_MAX=12(lz4-mt.h);LZ5、Lizard 结构与之一致。

4.2 解压端 API

BROTLIMT_DCtx *BROTLIMT_createDCtx(int threads, int threadsset, int inputsize); size_t BROTLIMT_decompressDCtx(BROTLIMT_DCtx *ctx, BROTLIMT_RdWr_t *rdwr); size_t BROTLIMT_GetFramesDCtx(BROTLIMT_DCtx *ctx); size_t BROTLIMT_GetInsizeDCtx(BROTLIMT_DCtx *ctx); size_t BROTLIMT_GetOutsizeDCtx(BROTLIMT_DCtx *ctx); void BROTLIMT_freeDCtx(BROTLIMT_DCtx *ctx);

解压端threads同样支持0:0表示以单线程方式处理不带 skippable 帧的原始 brotli 流(inputsize参数用于该模式的输入分块);1..128则按 MT 容器解析。值得强调的是 brotli-mt_decompress.c 中实现的自动内容识别:解压器会先把流的开头读入一块prebuf(256+16 字节的预读缓冲),尝试区分"裸 brotli 流"与"MT 容器流"——若流以0x184D2A50+size=8+BR魔法开头则走容器路径,否则按原始 brotli 处理。这样一份由 MT 压缩产生的文件,解压时即使调用方没有显式指定线程数,也能被正确识别并按 MT 方式并行解压。

4.3 回调式读写与 Buffer 结构

库不绑定任何具体 I/O,通过回调注入读写函数(头文件注释明确说明:可使用 stdio 函数或裸 read/write,自行编写包装;7-Zip ZS 中的封装即示例):

typedef struct { void *buf; /* 数据指针 */ size_t size; /* 当前已填充字节数 */ size_t allocated; /* buf 的分配长度 */ } BROTLIMT_Buffer; typedef int (fn_read) (void *args, BROTLIMT_Buffer *in); typedef int (fn_write) (void *args, BROTLIMT_Buffer *out); typedef struct { fn_read *fn_read; /* 读回调 */ void *arg_read; /* 读参数 */ fn_write *fn_write; /* 写回调 */ void *arg_write; /* 写参数 */ } BROTLIMT_RdWr_t;

约定:回调返回-1表示出错、返回0表示成功;实际读/写的字节数写入in->size/out->size。

4.4 错误处理

所有 API 的size_t返回值遵循"负数即错误"的约定:MT_ERROR(name)宏定义为((size_t)-BROTLIMT_error_##name),用BROTLIMT_isError(code)判断是否为错误码,用BROTLIMT_getErrorString(code)取可读错误字符串。错误码枚举(brotli-mt.h)覆盖了内存分配失败、读/写失败、畸形输入、数据意外结束、单帧压缩/解压失败、压缩参数越界、底层压缩库错误、操作被取消等场景,对应描述文本在 brotli-mt_common.c 中实现。

五、多线程工作模型:worker 循环与写队列

zstdmt/README.md 只定义了容器格式,而"如何并行"由源码实现决定。以 brotli-mt_compress.c 的头部注释为据,其工作模型是无主线程的纯多 worker 版本:每个线程自行循环执行四步——

  1. 加读锁(read_mutex)读取一块输入;
  2. 释放读锁,独立执行本块的压缩(调用BrotliEncoderCompress,输出缓冲按BrotliEncoderMaxCompressedSize(inputsize) + 16预留帧头空间,见 brotli-mt_compress.c);
  3. 加写锁(write_mutex)写入结果;
  4. 回到第 1 步,直到输入耗尽。

读锁保证多个 worker 不会读到同一段输入,写锁保证输出顺序不乱。

关键设计是三链表写队列(brotli-mt_compress.c 与 list.h 中的 Linux 风格双向链表实现):

  • writelist_free:空闲的输出缓冲,可被复用;
  • writelist_busy:worker 正在压缩中的缓冲;
  • writelist_done:压缩完成、等待按序写出的缓冲。

由于并行压缩的各帧完成顺序不定,pt_write()(brotli-mt_compress.c)通过每个writelist上记录的frame序号与上下文中的curframe匹配,保证输出流严格按帧序落盘:只有"当前应写出的帧"就绪时才调用写回调,否则等待;写出一帧后curframe++并继续检查后续帧是否也已就绪(goto again循环),从而在不阻塞并行压缩的前提下维护容器流的帧序不变。解压端(brotli-mt_decompress.c)采用同样的free/busy/done队列模型,只是每帧的解压结果同样按帧号顺序写出。

线程原语方面,threading.h 在 POSIX 系统直接使用pthread.h;在 Windows 上则通过 zstd-mt_threading.c 提供的 Pthread 包装实现兼容:互斥量映射为CRITICAL_SECTION,线程创建用_beginthreadex启动worker中转函数,pthread_join用WaitForSingleObject(handle, INFINITE)等待完成。字节序相关的读写(MEM_readLE32等)与likely/unlikely等宏集中在 memmt.h 中,保证帧头字段以 little-endian 稳定读写。

六、在 7-Zip-zstd 中的集成与验证

zstdmt 并不是孤立库,它已被完整接入本仓库的 7-Zip 构建体系:

  • 构建集成:多个构建目标把 C/zstdmt 下的源文件编入。例如 CPP/7zip/Bundles/Alone/makefile.gcc 通过$(wildcard ../../../../C/zstdmt/lz4*.c)收集 LZ4/LZ5 MT 源码并追加zstd-mt_threading.c;CPP/7zip/7zip.mak 也声明了$(ZSTDMT_OBJS)的目标规则。
  • 解压端自动识别:CPP/7zip/Archive/BrotliHandler.cpp 的注释明确说明:只有当用户显式用-mmt指定线程数时才向解码器强制传线程数;否则BROTLIMT_decompressDCtx()会自行检查流的 skippable-frame 容器魔法,自动决定按裸流单线程解压还是按容器多线程解压(有容器时自动提升到最大处理器数)。因此"用-mmt压缩出的文件,在未加-mmt的解压调用中也能被正确读取"。
  • 回归测试:仓库的 tests/regr-arc 目录下保留了test.txt.br-mt.br等多线程格式的测试样本,配合 tests/main.test 与 tests/7z-test.tcl 可对 MT 容器的压缩/解压与回退行为做端到端验证。

七、注意事项与兼容性边界

基于上述源码事实,使用 zstdmt 时有几点值得注意:

  1. 线程上限:Brotli/LZ4/LZ5/Lizard 的 MT 库统一以128为线程数上限,超限请求会在创建上下文阶段被拒绝(返回空指针)。
  2. threads=0的特殊语义:对 Brotli 等库而言,0不是"自动选择",而是"关闭容器、输出裸单线程流"。若想输出标准 MT 容器,必须传1..128。
  3. 帧头开销:通用容器每帧 12 字节、Brotli 每帧 16 字节,分块越小并行度越高但开销占比越大;inputsize默认按压缩级别自动放大(1MB × level),可视为库在并行度与压缩率之间的默认平衡点,调用者可自行覆盖。
  4. 解码兼容性:MT 容器复用了 Zstandard 的 Skippable Frame 魔法,标准 zstd 解码器会跳过这些帧;而各算法的原生解码器只能解裸流,要完整还原 MT 容器流,必须使用本库的解压接口或支持该容器的 7-Zip-zstd 构建。
  5. 许可:zstdmt 库本身以 BSD 协议发布,见 C/zstdmt/LICENSE;7-Zip-zstd 整合后的整体许可见仓库根目录 COPYING。

结语

从字节布局到线程调度,zstdmt 用一个 12/16 字节的 Skippable Frame 容器统一了五种压缩算法的多线程封装:12 字节通用头承载0x184D2A50魔法与压缩尺寸,Brotli 在此基础上追加BR魔法与 64KB 粒度的解压分配提示;每个 worker 通过"读锁取块 → 独立压缩 → 写锁按帧号顺序落盘"的循环,配合free/busy/done三链表队列维持输出帧序。理解这一容器格式,既有助于在 7-Zip-zstd 之外复用 C/zstdmt 的 API 构建自己的多线程压缩工具,也能让你在排查 MT 流问题时,直接按本文的字节表手工解析或构造帧头。

  • 桌面应用
  • CLI

【免费下载链接】7-Zip-zstd

7-Zip with support for Brotli, Fast-LZMA2, Lizard, LZ4, LZ5 and Zstandard

项目地址:https://gitcode.com/gh_mirrors/7z/7-Zip-zstd
点击查看免费下载

相关推荐

上一篇:Ollama日志轮转配置:磁盘空间优化与日志管理终极指南
下一篇:OHHTTPStubs Mocktail格式终极指南:简化iOS网络测试数据管理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询