一、Zstandard 是什么?我为什么推荐它?
压缩库有很多,zlib、bzip2、LZ4……但如果你问我个人最常用哪个,我会说 Zstandard(简称 Zstd)。
Zstd 是 Facebook 开源的一个实时压缩算法。它的核心特点是:压缩速度极快,压缩比也不差。说白了,它能在你几乎感觉不到延迟的情况下,把数据体积压下去一半甚至更多。
我在项目中遇到过几次这样的场景:服务器日志每小时生成几个 GB,直接存磁盘太浪费,用 gzip 压缩又太慢,CPU 飙得老高。后来换成 Zstd,压缩速度提升了 3-5 倍,CPU 占用反而降了。嗯,从那以后我就成了 Zstd 的忠实用户。
Zstd 的核心优势:
- 压缩速度可调(1-22 级,级别越高越慢但压缩比更好)
- 解压速度极快,且与压缩级别无关
- 自带字典训练功能,对小数据块特别友好
- CMake 集成非常简单,头文件 + 静态库即可
二、CMake 中集成 Zstd 的两种方式
集成 Zstd 其实就两条路:用系统预装库,或者自己编译源码。我个人更推荐后者,因为可控性更强,而且 Zstd 的源码编译非常轻量。
方式一:find_package 查找系统库
如果你的系统已经装了 Zstd(比如 Ubuntu 上 apt install libzstd-dev),那直接用 CMake 的 find_package 就行:
find_package(zstd REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE zstd::libzstd_shared)
这种方式最省事。但有个坑——不同发行版提供的 CMake 配置文件可能不一样。我曾经在 CentOS 7 上遇到过 find_package 找不到的情况,最后发现是包太老了,没有导出 CMake target。
注意:系统库的版本可能比较旧。Zstd 1.5.0 之后 API 有少量变化,如果你需要最新特性,建议用第二种方式。
方式二:FetchContent 自动下载编译
这是我最喜欢的方式。不需要用户手动安装任何东西,CMake 配置时自动下载源码并编译:
include(FetchContent)
FetchContent_Declare(
zstd
GIT_REPOSITORY https://github.com/facebook/zstd.git
GIT_TAG v1.5.6
)
# 注意:Zstd 的 CMake 项目在 build/cmake 目录下
set(ZSTD_LEGACY_SUPPORT OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(zstd)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE libzstd_static)
target_include_directories(my_app PRIVATE ${zstd_SOURCE_DIR}/lib)
这里有几个细节要注意:
- Zstd 的 CMakeLists.txt 藏在
build/cmake目录下,不是根目录。FetchContent 默认会去找根目录,所以需要额外处理。 - 我习惯关掉共享库和命令行工具,只编译静态库。这样部署时少操心。
- 头文件路径要手动加一下,因为 Zstd 的公共头文件在
lib/目录下。
三、实战:压缩与解压的完整示例
光说不练假把式。我们写一个完整的例子:从文件读取数据,用 Zstd 压缩,再解压回来验证。
3.1 压缩函数
#include <zstd.h>
#include <vector>
#include <cstdint>
std::vector<uint8_t> compress_data(const std::vector<uint8_t>& input, int level = 3) {
// 获取压缩后的最大可能大小
size_t max_dst = ZSTD_compressBound(input.size());
std::vector<uint8_t> compressed(max_dst);
// 执行压缩
size_t result = ZSTD_compress(
compressed.data(), compressed.size(),
input.data(), input.size(),
level
);
if (ZSTD_isError(result)) {
throw std::runtime_error(ZSTD_getErrorName(result));
}
// 调整到实际大小
compressed.resize(result);
return compressed;
}
这里有个小细节:ZSTD_compressBound 返回的是最坏情况下的压缩后大小。如果数据本身是随机的,压缩后反而可能变大。嗯,这种情况虽然少见,但你要有心理准备。
3.2 解压函数
std::vector<uint8_t> decompress_data(const std::vector<uint8_t>& compressed) {
// 先获取原始大小(Zstd 会把原始大小存在压缩数据头部)
unsigned long long original_size = ZSTD_getFrameContentSize(
compressed.data(), compressed.size()
);
if (original_size == ZSTD_CONTENTSIZE_UNKNOWN ||
original_size == ZSTD_CONTENTSIZE_ERROR) {
throw std::runtime_error("无法获取原始数据大小");
}
std::vector<uint8_t> decompressed(original_size);
size_t result = ZSTD_decompress(
decompressed.data(), decompressed.size(),
compressed.data(), compressed.size()
);
if (ZSTD_isError(result)) {
throw std::runtime_error(ZSTD_getErrorName(result));
}
return decompressed;
}
你想想看,解压时最怕什么?怕不知道原始大小。Zstd 很贴心,它把原始大小直接编码在压缩数据里,用 ZSTD_getFrameContentSize 就能取出来。不过要注意,如果数据是流式压缩的(比如网络传输场景),这个函数可能返回 ZSTD_CONTENTSIZE_UNKNOWN。这时候你就得用流式 API 了。
3.3 流式压缩(进阶)
对于大文件或网络流,一次性压缩整个数据块不太现实。这时候要用流式 API:
void stream_compress(FILE* input, FILE* output, int level = 3) {
ZSTD_CCtx* cctx = ZSTD_createCCtx();
ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, level);
char in_buf[4096];
char out_buf[ZSTD_compressBound(sizeof(in_buf))];
while (!feof(input)) {
size_t read_size = fread(in_buf, 1, sizeof(in_buf), input);
int last_chunk = feof(input);
ZSTD_EndDirective mode = last_chunk ? ZSTD_e_end : ZSTD_e_continue;
ZSTD_inBuffer in = {in_buf, read_size, 0};
ZSTD_outBuffer out = {out_buf, sizeof(out_buf), 0};
while (in.pos < in.size) {
ZSTD_compressStream2(cctx, &out, &in, mode);
fwrite(out_buf, 1, out.pos, output);
out.pos = 0;
}
}
ZSTD_freeCCtx(cctx);
}
流式 API 的核心是 ZSTD_compressStream2。它需要你反复调用,直到输入数据全部被消费。最后一个数据块要传 ZSTD_e_end 标记结束。我在做日志实时压缩时就用这个方案,效果非常好。
四、知识体系总览
下面这张图总结了 Zstd 在 CMake 项目中的集成路径和核心 API 关系:
五、避坑指南与性能调优
最后分享几个我踩过的坑:
技巧 1:压缩级别不是越高越好
我曾经为了追求极致压缩比,把级别设到 19。结果压缩一个 100MB 的文件花了 30 秒,而级别 3 只用了 0.5 秒,压缩比只差了 5%。所以日常场景用 3-5 级就够了,归档场景才考虑 19+。
避坑 2:多线程压缩需要显式开启
Zstd 支持多线程压缩,但默认是单线程。你需要调用 ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 4) 来启用。而且多线程只在压缩级别 1 和 2 上有效,高级别反而会退化为单线程。这个坑我查了半天文档才搞明白。
技巧 3:小数据块用字典压缩
如果你要压缩大量相似的小数据块(比如几千条 JSON 记录),用 Zstd 的字典训练功能可以大幅提升压缩比。训练过程也很简单:ZSTD_trainDictionary 函数,传入一批样本数据即可。我在做物联网设备数据上报时用过,压缩比从 2x 提升到了 8x。
好了,关于 Zstd 的 CMake 集成就聊到这里。代码不多,但每个细节都值得你亲手试一遍。特别是流式压缩那块,建议你写个小 demo 跑一跑,感受一下 Zstd 的速度。
公众号:蓝海资料掘金营,微信 deep3321