bigywater.tech
数据编码与压缩 · 1

怎么公平地比较编码与压缩:测试数据、指标与重复次数

比较编码和压缩之前,先把怎么测说清楚:用什么数据、怎么计时、压缩比怎么定义、哪些因素会让数字看起来不一样。这篇把后面所有篇目共用的方法和一些容易误会的地方先量一遍。

5.42× 对 6.66×
同一份数据压缩后 1331144 到 1441638 字节,只因基准是紧凑 JSON 还是 indent=2 的 JSON,压缩比从 5.42 变成 6.66
1.25× 对 5.40×
products 每条单独压缩 与 2 万条整批压缩 的 zstd-3 压缩比
1.00× 对 20971×
随机字节与全零数据在 zstd-3 下的压缩比,数据本身的重复程度决定上限

测试数据

实测 · 本机执行输出
  数据集           记录数  JSON Lines 字节     平均每条    不同的键名数  特点
  products    20000        7214161      360        12  爬虫抓到的商品记录:字符串多、字段名每条都重复
  timeseries  50000        2744541       54         4  数值密集:时间戳递增、数值随机游走
  nested       3000        5582050     1860         8  嵌套 5 层、键名大量重复的配置式文档
  blobby       3000         (含二进制)                  5  每条带 512 字节随机二进制缩略图(共 1500 KiB,不可压缩)
  pages        1500       20859437    13906            类 HTML 页面文本,每页约 13 KiB,用于纯压缩实验

所有数据集都由固定随机种子的脚本生成:products(爬虫商品)、timeseries(数值)、nested(嵌套配置)、blobby(含随机二进制)、pages(类 HTML 文本)。它们是合成数据,真实数据的压缩比会和这里有差距,每篇的结论只对这些数据成立。

计时

实测 · 本机执行输出
  CPU 核数 10;第一次 38.5 ms;之后 9 次:最小 37.3 / 中位 38.2 / 最大 38.7 ms(最大是最小的 1.04 倍)
  同时跑着 10 个空转进程:最小 37.2 / 中位 51.1 / 最大 71.6 ms(中位是无负载时的 1.34 倍,最大是最小的 1.92 倍)

计时用同一操作重复多次取中位数,并在机器空闲时测。实验里 json.dumps 在空闲时最大只是最小的 1.04 倍;同时开满 CPU 空转进程时,中位数变成 1.34 倍、最大值是最小值的 1.92 倍(三次运行里中位数的变化在 1.04 到 1.34 倍之间),所以不用单次或最大值。

压缩比的口径

实测 · 本机执行输出
  基准                                   原始字节   zstd-3 后     原始/压缩后     压缩后/原始     省下
  紧凑 JSON(无空格)                      7214163    1331144      5.42×      18.5%  81.5%
  默认 json.dumps(逗号冒号后带空格)           7754162    1347541      5.75×      17.4%  82.6%
  indent=2 的 JSON                   9594164    1441638      6.66×      15.0%  85.0%
  JSON Lines(紧凑)                    7214161    1331194      5.42×      18.5%  81.5%
  压缩后的字节数:紧凑 1331144、默认 1347541、indent=2 1441638——相差不大,"压缩比"主要是被不同的基准拉开的

“压缩比”可以写成原始/压缩后(5.42×)、压缩后/原始(18.5%)或省下的比例(81.5%)。更要紧的是基准:同一个压缩结果,拿 indent=2 的 JSON 当基准比拿紧凑 JSON 高出一大截。后面各篇一律给出压缩后的字节数,基准明确写成“紧凑 JSON”。

批大小

实测 · 本机执行输出
  每批     1 条:共  5771649 字节,压缩比  1.25×(原始 JSON 字节 7194162),平均每条  288.6 字节
  每批    10 条:共  2472557 字节,压缩比  2.91×(原始 JSON 字节 7194162),平均每条  123.6 字节
  每批   100 条:共  1746783 字节,压缩比  4.12×(原始 JSON 字节 7194162),平均每条   87.3 字节
  每批  1000 条:共  1421724 字节,压缩比  5.06×(原始 JSON 字节 7194162),平均每条   71.1 字节
  每批 20000 条:共  1331194 字节,压缩比  5.40×(原始 JSON 字节 7194162),平均每条   66.6 字节

压缩器只能利用它已经看到的内容。每条记录单独压缩时 2 万条只有 1.25×,整批压缩有 5.40×,每批 1000 条已经有 5.06×。小对象的压缩需要字典或攒批,见第 17 篇。

数据决定上限

实测 · 本机执行输出
  内容                                     gzip-6     zstd-3        lz4   (压缩比 = 1 MiB / 压缩后)
  随机字节 randbytes                          1.00×      1.00×      1.00×
  随机小写字母                                  1.57×      1.68×      1.00×
  词表里的词(4000 个词随机拼)                       2.52×      2.81×      1.34×
  products 的 JSON Lines(截 1 MiB)          4.82×      5.29×      3.10×
  重复的 1 KiB 块                           166.60×    927.12×    195.52×
  全零                                   1009.22×  20971.52×    241.61×

随机字节怎么压都是 1.00×;随机小写字母约 1.6×;词表里的词拼接约 2.8×;有结构的 JSON Lines 约 5.3×;重复的 1 KiB 块能到 927×。后面的“高压缩比是怎么做到的”几篇会逐个解释这些数字的来源。

可复现

实测 · 本机执行输出
  单线程 zstd-3 压缩 5 次:不同的输出有 1 种(['5f5df667a708'])
  threads=0/2/4 的输出:{0: (1331194, '5f5df667a708'), 2: (1331194, '5f5df667a708'), 4: (1331194, '5f5df667a708')};字节完全相同
  压缩 6.9 MiB 的中位耗时:threads=0 14.7 ms、threads=2 15.8 ms、threads=4 15.0 ms
  换成 19.9 MiB 的 pages 数据:threads=0 4151340 字节(54 ms)、threads=4 4147915 字节(26 ms);字节不同

同一个库、同一份数据、同一个级别,压缩输出是确定的。多线程压缩大数据(19.9 MiB)时输出会和单线程不同,体积也略有差别,需要字节级可复现(例如对压缩后的文件做校验和)时要注意。

分步演示

测试方法
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8(系统 Python,自带 compression.zstd)/ zstandard 0.25.0 / lz4 4.4.5 / zstd、lz4、brotli、protoc 命令行(Homebrew);msgpack、protobuf、snappy 等第三方库临时装在临时目录里,用完删除;所有数据都是本机脚本生成的合成数据(固定随机种子),没有使用任何真实网站或用户的数据;计时用 time.perf_counter,取多次中位数,数字每次运行会有小幅抖动。数据集由 ec.py 生成(固定种子 1 到 5);第二节重复 9 次,第三、四、五节各一次(压缩结果是确定的),第六节单线程重复 5 次、多线程各一次
    • 没有验证:真实站点数据的压缩比;其它压缩级别(这一篇只用了 zstd 3 和 gzip 6);多线程压缩开始生效的输入大小阈值;不同 CPU 架构和库版本下的数字差别;内存占用和能耗
    京ICP备2026067502号-1京公网安备11010502063487号