- 静态分析
- 开发工具
- 代码质量
【免费下载链接】pyre-check
Performant type-checking for python.
导读:本文以 pyre-check 仓库中 vendored 的 zstd(Zstandard)文档为核心,系统讲解这一高性能无损压缩算法在 Python 静态类型检查器中的真实落地方式。读者将掌握 zstd 的压缩/解压命令行用法、字典训练模式、多套构建方案,并深入理解 pyre-check 是如何通过 Hack Parallel 的共享堆(shared heap)把 zstd 变成跨进程共享内存的压缩"搬运工",从而支撑大规模类型检查的内存效率。
一、zstd 是什么:面向实时场景的高性能无损压缩
Zstandard(简称zstd)是一种快速的无损压缩算法,由 Facebook 开源,其设计目标是在接近 zlib 级别的压缩比下,提供面向实时压缩场景的高吞吐速度。与通用算法不同,zstd 在速度与压缩比之间提供了极其细粒度的可调空间,并且解压速度几乎不受压缩等级影响——这是它被广泛用于大数据存储、日志压缩、构建缓存、静态检查工具内部存储等场景的核心原因。
在 pyre-check 仓库中,zstd 以第三方 vendored 源码形式存在,位于 source/hack_parallel/hack_parallel/third-party/zstd,其根目录 README.md 完整阐述了算法特性、基准测试、字典训练与构建方法。同时,zstd 被 Hack Parallel(hack_parallel)模块实际使用:该模块是 Facebook 为 OCaml 服务/检查器开发的并行与共享内存基础设施,pyre-check 的 OCaml 核心(如source/analysis、source/server)正是构建在其上的。
1.1 技术构成与许可
zstd 项目提供:
- 一个以C 语言编写的库(
libzstd); - 一个命令行工具,可产生与解码
.zst、.gz、.xz、.lz4等格式文件; - 双许可(BSD / GPLv2)的开源授权,详见仓库内 LICENSE 与 COPYING;
- 其高速熵编码阶段由 Huff0 与 FSE(Finite State Entropy)库提供支撑,二者代码同样随 zstd 一起 vendored(见 lib/common/fse.h、lib/common/huf.h)。
1.2 源码目录结构
本仓库 vendored 的 zstd 采用模块化布局,与官方发布版一致:
| 目录 | 职责 |
|---|---|
lib/common/ | 所有构建变体都必需的公共基础,包括位流、FSE/HUF 熵编解码、xxhash、内存与线程工具等 |
lib/compress/ | 压缩实现,包含多套策略(zstd_fast.c、zstd_double_fast.c、zstd_lazy.c、zstd_opt.c)及多线程压缩zstdmt_compress.c |
lib/decompress/ | 解压实现,含zstd_decompress.c、zstd_decompress_block.c与字典相关zstd_ddict.c |
lib/dictBuilder/ | 字典训练器,含 COVER 与 fastCover 算法(cover.c、fastcover.c、divsufsort.c、zdict.c) |
根目录Makefile/lib/Makefile/dune | 构建脚本,dune用于在 OCaml 工程中接入 |
二、性能基准:速度与压缩比的权衡图谱
原文档记录了由官方在 Silesia 压缩语料上使用 lzbench 实测的一组数据(测试环境:Arch Linux 5.5.11、Core i9-9900K @ 5.0GHz、gcc 9.3.0),值得作为选型参考完整保留:
| Compressor name | Ratio | Compression | Decompress. |
|---|---|---|---|
| zstd 1.4.5 -1 | 2.884 | 500 MB/s | 1660 MB/s |
| zlib 1.2.11 -1 | 2.743 | 90 MB/s | 400 MB/s |
| brotli 1.0.7 -0 | 2.703 | 400 MB/s | 450 MB/s |
| zstd 1.4.5 --fast=1 | 2.434 | 570 MB/s | 2200 MB/s |
| zstd 1.4.5 --fast=3 | 2.312 | 640 MB/s | 2300 MB/s |
| quicklz 1.5.0 -1 | 2.238 | 560 MB/s | 710 MB/s |
| zstd 1.4.5 --fast=5 | 2.178 | 700 MB/s | 2420 MB/s |
| lzo1x 2.10 -1 | 2.106 | 690 MB/s | 820 MB/s |
| lz4 1.9.2 | 2.101 | 740 MB/s | 4530 MB/s |
| zstd 1.4.5 --fast=7 | 2.096 | 750 MB/s | 2480 MB/s |
| lzf 3.6 -1 | 2.077 | 410 MB/s | 860 MB/s |
| snappy 1.1.8 | 2.073 | 560 MB/s | 1790 MB/s |
对这张表需要正确解读:
- 默认等级
-1:压缩比 2.884,压/解速度分别为 500 MB/s 与 1660 MB/s,明显优于 zlib -1(压缩比 2.743,但压缩速度仅 90 MB/s)与 brotli -0; - 负压缩等级
--fast=#:通过牺牲少量压缩比换取更快的压缩与解压速度,从--fast=1到--fast=7,压缩速度从 570 MB/s 提升到 750 MB/s; - 解压速度保持特性:zstd 在所有设置下解压速度基本保持不变(约 1.6~2.5 GB/s 区间),这是它与 zlib、lzma 等绝大多数 LZ 系算法共享的特性,也是其适合"压缩一次、解压多次"场景的关键;
- 若需要更高压缩比,可通过提高压缩等级换取(速度换压缩比),其 trade-off 可以按很小的增量精细调节。
注意:以上数字来自原文档在特定硬件与编译器上的官方测试,是选型参考而非本仓库的实测值;实际表现会随 CPU、语料与编译选项变化。lzbench 是开源的 in-memory 基准工具,Silesia 压缩语料是公开的标准测试集,用于横向对比各算法。
三、小数据压缩与字典训练:为什么小文件需要"预热"
通用压缩算法都是"从过去学习如何压缩未来":数据越小,可供学习的"过去"越少,压缩效果越差。zstd 专门为此提供了训练模式(training mode):
- 通过给算法提供一批同类型样本(每个样本一个文件),生成一个称为dictionary(字典)的产物文件;
- 压缩与解压前都必须先加载该字典;
- 使用字典后,小数据的压缩比提升非常显著,且压缩与解压速度同时变快;
- 字典只在样本族存在相关性时有效;字典越贴近特定数据类型越高效——不存在"万能字典",因此每类数据部署一份专属字典收益最大;
- 字典收益主要集中在前几 KB,后续算法会逐步用已解码内容继续压缩剩余部分。
官方示例使用 github-users 样本集(约 1 万条记录、每条约 1KB)展示了字典带来的压缩比、压缩速度、解压速度三方面的收益(详见原文档图表)。
3.1 字典压缩三步走(原文档 How To)
创建字典
zstd --train FullPathToTrainingSet/* -o dictionaryName使用字典压缩
zstd -D dictionaryName FILE使用字典解压
zstd -D dictionaryName --decompress FILE.zst
在库 API 层面,字典能力由 lib/dictBuilder/zdict.h 暴露(ZDICT_trainFromBuffer等接口),训练算法为lib/dictBuilder/cover.c中的 COVER 算法与fastcover.c中的 fastCover 变体,它们为后续章节将讲到的 pyre-check 内嵌字典提供了算法基础。
四、构建 zstd:Makefile / cmake / Meson / vcpkg / VS / Buck 全方案
原文档给出了完整的多平台构建路径,全部适用于本仓库中的 vendored 源码(构建时在source/hack_parallel/hack_parallel/third-party/zstd目录下操作):
| 构建方式 | 命令/入口 | 产出 |
|---|---|---|
| Makefile | make(根目录) | 根目录生成zstdCLI |
| Makefile 扩展 | make install | 安装 CLI、库与 man pages |
| Makefile 扩展 | make check | 构建并运行本地平台行为测试 |
| cmake | build/cmake工程生成器 | 生成 Makefile 或其他构建脚本,产出zstd二进制与libzstd动态/静态库;默认CMAKE_BUILD_TYPE=Release |
| Meson | build/meson目录(默认 release 构建) | 参考其中构建说明 |
| vcpkg | ./vcpkg install zstd | 通过 vcpkg 依赖管理器安装 |
| Visual Studio | build目录内 VS2005/2008/2010 工程(VS2010 兼容 VS2012~VS2017),以及build/VS_scripts自动化脚本 | zstd CLI 与 libzstd 库 |
| Buck | buck build programs:zstd | 输出于buck-out/gen/programs/ |
4.1 库级构建的更多细节(lib/README)
仓库内的 lib/README.md 补充了库级(libzstd)构建的关键约定:
make lib:同时生成静态库与动态库;默认动态库为多线程、静态库为单线程(兼容性考虑);- 多线程需要两个条件:定义
ZSTD_MULTITHREAD宏(gcc 用-DZSTD_MULTITHREAD),且 POSIX 系统编译时加-pthread;链接多线程版时同样需-pthread; - 可通过目标后缀强制切换:
make lib-mt(双库多线程)、make lib-nomt(双库单线程); - 稳定 API 位于
lib/zstd.h;lib/zstd_errors.h把size_t结果映射为ZSTD_ErrorCode便于错误处理; - 若在包含
zstd.h之前定义ZSTD_STATIC_LINKING_ONLY,可解锁实验性 API,但实验 API 不稳定,只允许静态链接,绝不可用于动态库。
4.2 模块化裁剪:按需编译子集
libzstd支持只编译所需功能,目录设计天然支持这一点:
lib/common是所有变体必须包含的公共基础;lib/compress与lib/decompress彼此独立,可只取其一;lib/dictBuilder依赖common+compress,提供字典生成 API(zdict.h);- 通过
make libzstd时定义ZSTD_LIB_COMPRESSION=0、ZSTD_LIB_DECOMPRESSION=0、ZSTD_LIB_DICTBUILDER=0、ZSTD_LIB_DEPRECATED=0可裁剪相应功能及其依赖(例如关闭压缩也会连带禁用 dictBuilder); - 体积优化:设置
ZSTD_LIB_MINIFY=1会禁用可选组件并把编译选项切换为省空间优先;进一步可用HUF_FORCE_DECOMPRESS_X1/X2强制只编译某一种 Huffman 解码实现、ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT/LONG只保留一种序列解码实现、ZSTD_NO_INLINE禁用内联、ZSTD_STRIP_ERROR_STRINGS去掉错误字符串,并用-flto与--gc-sections等链接级手段做最终裁剪; DYNAMIC_BMI2=1/0可强制/禁止生成运行时 BMI2 指令检测分派(x64 + clang/gcc >= 5 时默认自动开启);- 构建时还可用
HASH变量(如make HASH=xxhsum)区分不同编译标志下的目标文件,用BUILD_DIR手动指定目标文件目录。
五、pyre-check 中 zstd 的实战应用:共享内存堆的压缩存取
如果说前三部分是 zstd 作为通用库的"通用能力",那么本章就是它在 pyre-check 里的"定制化落地"——这也是本篇文章与纯 zstd 文档最大的差异点。
5.1 Hack Parallel 与共享堆
Hack Parallel(source/hack_parallel/hack_parallel)是 Facebook 用于 OCaml 并行/分布式服务的共享内存与进程管理基础设施。pyre-check 的 OCaml 核心(source/analysis、source/server、source/interprocedural等)正是建立在其上的。其中:
- source/hack_parallel/hack_parallel/heap/hh_shared.c 实现了跨进程共享的堆与哈希表;
- source/hack_parallel/hack_parallel/heap/dune 将
hh_shared、hh_shared_sqlite、dictionary_compression_data等编译进heap模块,并通过(libraries ... zstd lz4 sqlite3 ...)显式链接 zstd 库; - 该 heap 被 OCaml 侧通过 C stub 调用,用于存放类型检查过程中的各种大数据对象。
5.2 关键实现:压缩上下文与内嵌字典
在 hh_shared.c 中可以看到:
- 全局维护一个
ZSTD_CCtx*压缩上下文与一个ZSTD_DCtx*解压上下文(进程生命周期内常驻); - 压缩等级被固定为
zstd_compression_level = 5,源码注释明确说明"等级越低越快(以压缩比为代价)"——等级 5 是一个兼顾速度与压缩比的中间档位; - 初始化函数
init_zstd_compression()(hh_shared.c#L554-L576)在共享全局初始化后、fork worker 前被调用,创建 CCtx/DCtx 并把预训练字典挂接到上下文上:ZSTD_createCDict(dictionary_compression_data, dictionary_compression_data_length, zstd_compression_level)创建压缩字典,再经ZSTD_CCtx_refCDict绑定到压缩上下文;ZSTD_createDDict(...)创建解压字典,再经ZSTD_DCtx_refDDict绑定到解压上下文;
- 该预训练字典数据来自 source/hack_parallel/hack_parallel/heap/dictionary_compression_data.c:一段
112640字节的unsigned char数组,由 dictionary_compression_data.h 声明导出。
这正是原文档"字典训练"章节在 pyre-check 中的直接落地:对 OCaml 序列化对象这种高度同构的数据流,使用共享字典能在小对象上获得远优于无字典的压缩比——对照原文档结论"压缩收益同时带来更快的压缩与解压速度",等级 5 + 预训练字典的组合正是为此设计的。
5.3 写入路径:序列化后压缩再入堆
写入时(hh_store_ocaml,hh_shared.c#L1160-L1222):
- 若数据是 OCaml 字符串,直接取其内容;否则先用
caml_output_value_to_malloc序列化到 malloc 内存(KIND_SERIALIZED); - 用
ZSTD_compressBound(size)预分配最大压缩尺寸缓冲; - 调用
ZSTD_compress2(zstd_compression_context, ...)完成带字典的压缩; - 仅当压缩后更小(
compressed_size != 0 && compressed_size < size)才采用压缩数据,否则存原始数据;压缩后的原始大小记录在 header 的uncompressed_size字段(size << 33 | kind << 32 | uncompressed_size << 1 | 1); - 通过
hh_alloc在共享堆中分配条目并memcpy拷贝数据。
注意ZSTD_compress2正是实验/高级 API(ZSTD_STATIC_LINKING_ONLY解锁区域)中的函数,且上下文已通过ZSTD_CCtx_refCDict绑定字典,因此无需在每次调用时重复传入字典——一次绑定、反复使用,避免每对象重建字典的开销。
5.4 读取路径:按需解压
读取时(hh_deserialize,hh_shared.c#L1444-L1464):
- 从条目 header 读出
Entry_size与Entry_uncompressed_size; - 若
uncompressed_size_exp非零(说明存储的是压缩数据),malloc出原始大小缓冲,调用ZSTD_decompressDCtx(zstd_decompression_context, data, uncompressed_size_exp, src, size)解压,并断言解压后大小与记录一致; - 按
KIND_STRING(拷贝为 OCaml 字符串)或KIND_SERIALIZED(caml_input_value_from_block反序列化)还原对象。
整条链路因此是:OCaml 对象 → 序列化 → zstd(等级5+字典) 压缩 → 共享内存;读取时 zstd 解压 → 反序列化/字符串化 → OCaml 对象。得益于 zstd"解压速度与压缩等级无关"的特性,即使堆内数据量大、压缩等级为 5,worker 进程的读取延迟依然可控。
5.5 与 lz4/sqlite3 的并存
heap/dune 同时链接了zstd lz4 sqlite3:zstd 用于常规对象堆的压缩存取,lz4(另一种高速压缩算法)与 sqlite3 服务于 heap 模块中的其他存储路径。这说明 Hack Parallel 的存储层是"按场景选算法"的,zstd 的字典能力恰好覆盖了 OCaml 序列化数据这种高相似度流。
六、测试与质量保障
原文档说明:
- 快速冒烟测试:从
src/tests目录执行playTest.sh,需要两个环境变量$ZSTD_BIN与$DATAGEN_BIN分别指向 zstd 与 datagen 二进制; - 持续集成测试详见
TESTING.md。
在 pyre-check 中,zstd 的集成正确性由 OCaml 测试体系间接保障——heap 模块(hh_shared)是source/analysis、source/server等所有类型检查路径的底层依赖,仓库内 source/test 与 source/server/test 等测试目录会覆盖经共享堆读写对象的行为;make check(见第四节)则负责 zstd 库自身的本机行为验证。具体 CI 细节仍以官方 TESTING.md 与仓库构建脚本为准。
七、状态与许可总结
- 原文档明确:Zstandard 已在 Facebook 内部大规模部署,用于以多种格式与场景持续压缩大量数据,被认为是生产环境安全可用的;
- zstd 采用BSD 与 GPLv2 双许可(LICENSE、COPYING),集成方需按自身合规策略择一遵守;
- 贡献流程上,官方要求提交合并到
dev分支(或独立 feature 分支),不允许直接提交release(详见 CONTRIBUTING.md)。
八、从文档到源码的延伸阅读
| 想深入了解的内容 | 仓库内入口 |
|---|---|
| zstd 算法、基准与字典训练完整说明 | third-party/zstd/README.md |
| libzstd 构建、模块化裁剪、多线程与体积优化 | third-party/zstd/lib/README.md |
| 稳定/高级 API 与错误码 | third-party/zstd/lib/zstd.h、lib/zstd_errors.h |
| 字典训练算法(COVER/fastCover) | third-party/zstd/lib/dictBuilder/cover.c、fastcover.c |
| 熵编码(FSE/HUF) | third-party/zstd/lib/common/fse.h、lib/common/huf.h |
| pyre-check 中的压缩存取实现 | heap/hh_shared.c(压缩上下文 L385-L576、写入 L1160-L1222、读取 L1444-L1464) |
| pyre-check 内嵌预训练字典数据 | heap/dictionary_compression_data.c、heap/dictionary_compression_data.h |
| Hack Parallel 模块链接配置 | heap/dune |
综上所述:zstd 文档提供的是"通用压缩引擎 + 字典训练 + 多构建系统"的完整能力图谱,而 pyre-check 通过 Hack Parallel 把这份能力内化为"共享内存中高性能压缩存取"的工程实践——理解这两层,你就同时掌握了 zstd 的用法,以及大型类型检查器如何用压缩换内存的经典思路。
- 静态分析
- 开发工具
- 代码质量
【免费下载链接】pyre-check
Performant type-checking for python.
相关推荐
Zstandard(zstd)压缩算法全解析:原理、字典训练与构建实战
Zstandard(zstd)压缩算法全解析:原理、字典训练与构建实战 Zstandard(简称 zstd )是 Facebook 开源的一种快速无损压缩算法,
网络通信移动开发深入理解 Zstandard(zstd)压缩算法:从快速无损压缩原理到 Fluent Bit 中的集成实践
深入理解 Zstandard(zstd)压缩算法:从快速无损压缩原理到 Fluent Bit 中的集成实践 导读 :Zstandard(简称 zstd)是一款面
可观测性日志分析云原生流处理gh-ost 项目内嵌 zstd 纯 Go 压缩库实战指南:klauspost/compress/zstd 的压缩、解压与底层原理
gh ost 项目内嵌 zstd 纯 Go 压缩库实战指南:klauspost/compress/zstd 的压缩、解压与底层原理 导读 本文以 gh ost
数据库运维
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考