本系列的方法篇:五个合成数据集、计时用中位数且机器要空闲、同一个压缩结果换基准压缩比从 5.42× 变成 6.66×、每条单独压缩只有 1.25× 而整批压缩是 5.40×、数据越重复压缩比越高、多线程压缩大数据输出会和单线程不同。
量 JSON 的真实开销:2 万条商品记录里键名占 26.1%、字符串值占 59.3%、数字只占 5.7%;中文用 ensure_ascii=True 翻倍;node 会丢 2^53+1 的最后一位;orjson 比标准库快 2 到 8 倍;缩短键名省 15.2% 的原始字节,zstd 压缩后只剩 3.3%。
实测 MessagePack 对 JSON:未压缩小 11.9%–27.2%,但 products(字符串多)用 zstd 压缩后 MessagePack 反而大 6.2%,数值多的 timeseries 小 15.3%;速度比标准库 json 快、比 orjson 慢;整数键、bytes 能保留,tuple 变 list,2^64 溢出。
自己写 varint 和 ZigZag 并与 protobuf 库逐值对照:int32 的 -1 要 10 字节、sint32 只要 1 字节;大数值(时间戳)用 varint 比定长 4 字节还大;时间戳列先差分再压缩从 46185 降到 9005 字节;已知小数位数时乘 100 取整比 float64 小得多。
逐字节拆解一条 Protobuf 记录并与 protoc --decode_raw 核对:字段头 1 字节 = 字段号和类型,字段号 16 起多 1 字节(2 万条多 299953 字节);Protobuf 比 JSON 小 32.2%,但 zstd 压缩后反而大 3.3%;加字段、改名新旧兼容,int64 的大值被 int32 读成 0;默认值不写出。
有 schema 和无 schema 的对比:Protobuf 在赋值时拦住类型错误但不拦缺字段;没有 schema 看不出字符串和嵌套消息的区别;把键名挪到表头的 JSON 数组比 Protobuf 只大 2.4%、压缩后还更小;三种格式遇到翻转的字节都大多静默解出错误的值;Protobuf 解码到内存 11 MiB 对 JSON 36 MiB。
只想读一个字段或一条记录要解多少:整批解析 4–43 ms,带偏移索引的逐条存储只要 0.001–0.003 ms;求一列的和 Protobuf 3.9–6.3 ms 对 JSON 42.8 ms;字节级扫描 JSON 会被空白格式和嵌套同名键骗;压缩分块越大压缩比越高、点查越慢。
同样的数据换一种排列:列式比行式 zstd-3 小 33.2%;每列单独看,url 能压 94.67×、title 只有 2.96×;给每列配专用编码(字典、差分、整数分)再压缩共 685160 字节,比行式小 48.5%;读 price 一列只要 1.3 ms 对 57.7 ms;分块越小列式优势越小。
压缩比的天花板:随机字节熵 8 bit/字节谁也压不动;products 文本 0 阶熵下限 673018 字节、实际 xz-9 做到 113026;已知前 4 个字节条件熵降到 0.734;16.5 万个短字节串压缩后一个都没变短;概率很偏的信源 zstd 离理论值还差 66.5%。
自己写 Huffman:abracadabra 压成 23 位(定长 88 位);products 文本平均码长只比熵多 0.53%,但压缩比只有 1.55×;分布偏时(0.9/0.1)Huffman 比熵多 113.2%;只做 Huffman 的 zlib 是 1.55×,加上 LZ77 匹配是 4.90×。
自己写 LZ77 并解回原文验证:products 文本 95.9% 的字节被匹配覆盖,匹配距离多在 4 KiB 以上;窗口 256 字节估算 79440、窗口 128 KiB 估算 28042;搜索越努力越小;全零数据 zlib 最多约 1000×、zstd 可到 32466.8×;LZ77 加 Huffman 的估算与 zlib 级别 9 只差 1%。
自己写 inflate 解码真实的 deflate 流:gzip 比裸 deflate 多 18 字节(10 头 + 8 尾);products 1 MiB 压成 8 个动态块、95.4% 的输出由匹配产生;块头只占 0.2%;短输入或随机数据会改用固定码或存储块,最多多几个字节;gzip.GzipFile 默认会把当前时间写进头里。