一、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 关系:

Zstd 集成知识体系 集成方式 find_package 系统库 FetchContent 源码编译 核心 API 单次压缩:ZSTD_compress 流式压缩:ZSTD_compressStream2 解压:ZSTD_decompress 应用场景 日志压缩 网络传输 数据持久化 提示:压缩级别 1-3 适合实时场景,19-22 适合离线归档 解压速度与压缩级别无关,始终很快

五、避坑指南与性能调优

最后分享几个我踩过的坑:

技巧 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