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

窗口与内存:往回看得越远压缩得越小,代价是什么

往回看得越远,压缩得越小,代价是内存。这篇量窗口多大才找得到重复、窗口开大了为什么还可能找不到、解压端为什么会报“需要太多内存”,以及每个对象单独压缩要付出多少压缩比。

windowLog 21
重复内容相隔 1 MiB 时,zstd 级别 3 开始能匹配的最小窗口(2 MiB);windowLog 20 找不到
Frame requires too much memory
压缩端用了 windowLog 28,命令行 zstd -d 默认解压失败的错误;加 --long=28 才行
62.0%
8 MiB pages 切成 4 KiB 的块各自压缩,比整块压缩大的比例

窗口要大于重复的间隔

实测 · 本机执行输出
  1 MiB 页面文本单独压缩(级别 3):209800 字节;下面每个数据理想情况(第二份完全被匹配掉)约为 单份 + 填充
        填充      填充之外的字节    wLog 17    wLog 19    wLog 20    wLog 21    wLog 22    wLog 24    wLog 27
    0.00MiB    (压缩后减去填充)     466041     415169     417075     209905     209905     209905     209905
    0.25MiB    (压缩后减去填充)     466137     416400     417882     210059     210058     210058     210058
    1.00MiB    (压缩后减去填充)     466419     419030     419970     419315     210326     210326     210326
    4.00MiB    (压缩后减去填充)     467562     420173     421113     421113     421113     211459     211459
   16.00MiB    (压缩后减去填充)     472050     424661     425601     425601     425601     425601     425600
  (表里的数字是 压缩后总字节 - 填充字节:约等于 209800 表示第二份被匹配掉了,约等于 419600 表示第二份没有找到(重复压了一遍))

匹配只能指向窗口内的内容。重复的内容相隔 1 MiB,windowLog 20(1 MiB)找不到,windowLog 21(2 MiB)才行;相隔 5 MiB 要 windowLog 24。对 gzip 来说窗口固定是 32 KiB,更远的重复统统错过。

窗口大也不一定找得到

实测 · 本机执行输出
  级别 3,windowLog 27,不开 LDM                → 17202831 字节(425615 字节减去填充),22 ms
  级别 3,windowLog 27 + enable_ldm          → 16993311 字节(216095 字节减去填充),37 ms
  级别 3,windowLog 20(1 MiB)+ enable_ldm    → 17203150 字节(425934 字节减去填充),36 ms

相隔 16 MiB 的重复,级别 3 开了 windowLog 27 仍然没找到,因为搜索用的哈希表容量有限,老位置被覆盖。长距离匹配(enable_ldm)另建结构专门找远处的重复,开了才找到;但 LDM 仍受窗口限制。

解压端的代价

实测 · 本机执行输出
  windowLog 22 流式压成 17198867 字节,帧头声明的窗口 4 MiB
  windowLog 27 流式压成 16987969 字节,帧头声明的窗口 128 MiB
  windowLog 28 流式压成 16987969 字节,帧头声明的窗口 256 MiB
  帧 windowLog 22,解压端 max_window_size = 4 MiB:成功,输出 18874368 字节(一致)
  帧 windowLog 22,解压端 max_window_size = 64 MiB:成功,输出 18874368 字节(一致)
  帧 windowLog 22,解压端 max_window_size = 128 MiB:成功,输出 18874368 字节(一致)
  帧 windowLog 27,解压端 max_window_size = 4 MiB:失败:ZstdError: zstd decompressor error: Frame requires too much memory for decoding
  帧 windowLog 27,解压端 max_window_size = 64 MiB:失败:ZstdError: zstd decompressor error: Frame requires too much memory for decoding
  帧 windowLog 27,解压端 max_window_size = 128 MiB:成功,输出 18874368 字节(一致)
  帧 windowLog 28,解压端 max_window_size = 4 MiB:失败:ZstdError: zstd decompressor error: Frame requires too much memory for decoding
  帧 windowLog 28,解压端 max_window_size = 64 MiB:失败:ZstdError: zstd decompressor error: Frame requires too much memory for decoding
  帧 windowLog 28,解压端 max_window_size = 128 MiB:失败:ZstdError: zstd decompressor error: Frame requires too much memory for decoding
  命令行 zstd -d:windowLog 22 的帧:退出码 0,输出 18874368 字节,stderr:''
  命令行 zstd -d:windowLog 28 的帧:退出码 1,输出 0 字节,stderr:'/Users/w/Desktop/zen/tools/articles/ptmp/e16/w28.zst : Decoding error (36) : Frame requires too much memory for decoding \n/Users/w/Desktop/zen/tools/a'
  命令行 zstd -d --long=28:windowLog 28 的帧:退出码 0,输出 18874368 字节

压缩端声明的窗口决定了解压端需要的内存。windowLog 27 的帧在解压端限制 64 MiB 时失败,windowLog 28 的帧命令行默认就解不了,要 --long=28。给别人分发压缩文件时,窗口不要开得比对方的内存大。

和 gzip 的比较

实测 · 本机执行输出
  zlib-9(窗口 32 KiB):1822313 字节(压缩比 4.60×)
    zstd 级别    wLog 15    wLog 17    wLog 20    wLog 23
          3    2175415    1864670    1660885    1676095
          9    1839849    1590804    1558895    1560280
         19    1747221    1494076    1444525    1401606

同样 32 KiB 窗口,zstd-19 比 zlib-9 只小 4%;放开窗口,zstd-3 在 1 MiB 窗口下就比 zlib-9 小 9%。zstd 比 gzip 小,窗口是原因之一。

内存

实测 · 本机执行输出
  windowLog       窗口       压缩后      压缩多用内存       流式解压多用内存
         15    0.03M   5407003      13.8 MiB          0.2 MiB   解出长度一致
         18    0.25M   4112979      11.2 MiB          0.5 MiB   解出长度一致
         21    2.00M   4173277      12.9 MiB          2.5 MiB   解出长度一致
         24   16.00M   4177740      27.2 MiB         16.5 MiB   解出长度一致
         27  128.00M   4172915      30.7 MiB         20.4 MiB   解出长度一致

窗口越大,压缩和解压需要的内存越多(流式解压 0.2 MiB 到 20.4 MiB),但窗口超过数据中重复的间隔后,压缩后的大小就不再改善(windowLog 18 之后基本不变)。

不要每个对象单独压缩

实测 · 本机执行输出
        每块大小     块数       压缩后总字节     压缩比      比一整块大
         4KiB   2048      2705807   3.10×      62.0%
        16KiB    512      2376431   3.53×      42.3%
        64KiB    128      2007345   4.18×      20.2%
       256KiB     32      1759232   4.77×       5.3%
      1024KiB      8      1679330   5.00×       0.5%
      8192KiB      1      1670198   5.02×       0.0%

每个对象单独压缩等于每次重置窗口。4 KiB 的块压缩比 3.10×,整块是 5.02×;块到 256 KiB 才接近(4.77×)。小对象要攒批或用字典(下一篇)。

分步演示

窗口与内存
本步的对象

    本文用到的工具与边界

    • 本机 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,取多次中位数,数字每次运行会有小幅抖动。zstandard 0.25.0(libzstd 1.5.7);第一、二节的数据由固定种子的页面文本和随机填充组成,窗口、LDM 等参数用 ZstdCompressionParameters 指定;第三节的流式帧用 compressobj 生成(声明大窗口),并用命令行 zstd 核对;第五节用独立子进程测 ru_maxrss 增量;所有字节数是确定性的,时间是单次测量
    • 没有验证:LDM 的具体参数(ldm_hash_log、ldm_min_match 等)对结果的影响;哈希表容量(hashLog)到底怎么限制了找得到的距离;压缩端 windowLog 比数据短时帧头声明窗口变小的规则(只看到了现象);其它压缩器(xz、brotli)的窗口行为;多线程压缩时各段之间的窗口共享
    京ICP备2026067502号-1京公网安备11010502063487号