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

MessagePack 比 JSON 小多少,哪些数据几乎没变小

MessagePack 常被说成“更小更快的 JSON”。这篇用 4 个数据集量一量:未压缩和压缩之后各小多少,为什么有的数据几乎没变小,速度比谁快,类型保真和流式读取有什么不同。

11.9% 对 27.1%
未压缩时 MessagePack 比 JSON 小的比例:字符串多的 products 与数值多的 timeseries
+6.2%
products 用 zstd-3 压缩后,MessagePack 比 JSON 反而大的比例
13.7 对 5.0 ms
products 打包:msgpack 与 orjson 的耗时(标准库 json 是 40.8 ms)

编码规则

实测 · 本机执行输出
  整数 0                            1 字节  00
  整数 127                          1 字节  7f
  整数 128                          2 字节  cc 80
  整数 255                          2 字节  cc ff
  整数 256                          3 字节  cd 01 00
  整数 65535                        3 字节  cd ff ff
  整数 65536                        5 字节  ce 00 01 00 00
  整数 -1                           1 字节  ff
  整数 -32                          1 字节  e0
  整数 -33                          2 字节  d0 df
  整数 2**32                        9 字节  cf 00 00 00 01 00 00 00 00
  浮点 1.5                          9 字节  cb 3f f8 00 00 00 00 00 00
  浮点 0.1                          9 字节  cb 3f b9 99 99 99 99 99 9a
  True / None                     3 字节  92 c3 c0
  字符串 "a"                         2 字节  a1 61
  字符串 31 字节                      32 字节  bf 78 78 78 78 78 78 78 78 78 78 78 …
  字符串 32 字节                      34 字节  d9 20 78 78 78 78 78 78 78 78 78 78 …
  字符串 255 字节                    257 字节  d9 ff 78 78 78 78 78 78 78 78 78 78 …
  字符串 256 字节                    259 字节  da 01 00 78 78 78 78 78 78 78 78 78 …
  bytes 3 字节                      5 字节  c4 03 61 62 63
  {"a":1}                         4 字节  81 a1 61 01
  [1,2,3]                         4 字节  93 01 02 03
  JSON 对照:127 → 3 字节、65536 → 5 字节、"x"*31 → 33 字节、{"a":1} → 7 字节、0.1 → 3 字节

小整数 1 字节,更大的整数 2、3、5、9 字节;字符串头 1 到 3 字节;浮点一律 9 字节;容器头 1 字节起。JSON 里短的浮点(0.1 只要 3 字节)和 {"a":1}(7 字节)对比 MessagePack 的 9 字节和 4 字节,小整数、短键名占优的是 MessagePack,短浮点占优的是 JSON。

四个数据集

实测 · 本机执行输出
  数据集               JSON   MsgPack     省下   压缩后 zstd-3:      JSON   MsgPack      差   gzip-6:      JSON   MsgPack   lz4:      JSON   MsgPack
  products       7214163   6357308  11.9%                    1331144   1413136  -6.2%              1484671   1496373          2314042   2385707
  timeseries     2744543   2000003  27.1%                     352531    298689  15.3%               313607    321012           649008    580243
  nested         5582052   4062921  27.2%                     660737    606310   8.2%               553565    546978          1117578   1047168
  blobby         2224945   1652673  25.7%                    1578156   1579529  -0.1%              1592993   1582706          2117830   1605618

未压缩时 MessagePack 小 11.9%(products)到 27.2%(nested)。但 zstd 压缩之后差距变了:products 上 MessagePack 反而大 6.2%,timeseries 小 15.3%,nested 小 8.2%,含随机二进制的 blobby 基本相同。

为什么差别这么大

实测 · 本机执行输出
  products:MessagePack 逐值统计合计 6357305 字节(整批打包 6357308 字节,差在数组头);字符串 64.8%;键名 25.5%;浮点 5.7%;整数 3.1%;容器头 0.9%
  timeseries:MessagePack 逐值统计合计 2000000 字节(整批打包 2000003 字节,差在数组头);键名 57.5%;浮点 22.5%;整数 17.5%;容器头 2.5%
  products:JSON 7214163 → MsgPack 6357308,省下 11.9%;timeseries:2744543 → 2000003,省下 27.1%

字符串多的数据(products 字符串占 64.8%),MessagePack 没什么可省;数值多的数据(timeseries 键名 57.5%、浮点和整数共 40%)省得多。数字越多,受益越大。

速度

实测 · 本机执行输出
  products:json.dumps 40.8 ms;orjson.dumps 5.0 ms;msgpack.packb 13.7 ms;json.loads 45.0 ms;orjson.loads 21.7 ms;msgpack.unpackb 32.9 ms
  timeseries:json.dumps 25.2 ms;orjson.dumps 3.3 ms;msgpack.packb 8.7 ms;json.loads 25.4 ms;orjson.loads 10.5 ms;msgpack.unpackb 16.3 ms

msgpack 比标准库 json 快(打包约 3 倍),但比 orjson 慢。“二进制就是快”不成立,要看对比的是哪个 JSON 实现。

类型

实测 · 本机执行输出
  tuple (1, 2)         3 字节 → [1, 2]
  整数键 {1: "a"}         4 字节 → {1: 'a'}
  bytes b"ab"          4 字节 → b'ab'
  str "ab"             3 字节 → 'ab'
  2**63                9 字节 → 9223372036854775808
  2**64                打包抛出 OverflowError
  float nan            9 字节 → nan
  datetime             打包抛出 TypeError
  set {1,2}            打包抛出 TypeError
  use_bin_type=False(旧格式):bytes 和 str 打包成同一类型 → {'a': 'ab', 'b': 'ab'}
  默认 strict_map_key=True 解包整数键:ValueError

MessagePack 比 JSON 多了 bytes 和整数键(解包要 strict_map_key=False),tuple 会变成 list,2^64 溢出,datetime 和 set 要自己处理。bytes 和 str 的区分需要 use_bin_type=True。

流式读取

实测 · 本机执行输出
  MessagePack 流 637751 字节(没有分隔符),按 1000 字节切成 638 块喂 Unpacker:取出 2000/2000 条;JSON Lines 723399 字节(靠换行分隔)
  把流截断在中间(多出半条):Unpacker 取出 1001 条完整记录,最后半条不会报错,只是留在缓冲里(再喂剩下的字节就能取出)
  JSON Lines 截断在同样位置:按行解析,1001/1002 行可解析(最后一行是半行,json.loads 抛出 JSONDecodeError)

每个 MessagePack 值自带长度,多条记录直接拼接,用 Unpacker 分块喂入就能取出(实验里 2000/2000)。JSON Lines 靠换行分隔。被截断的最后半条,Unpacker 不报错,只是还不能取出。

为什么压缩后反而更大

实测 · 本机执行输出
  版本                                      JSON   MsgPack  zstd JSON   zstd Msg    MsgPack 比 JSON(压缩后)
  原样(price/rating 是浮点)                 7214163   6357308    1331144    1413136                  +6.2%
  price→整数分、rating→整数                  7176128   6077249    1336116    1388457                  +3.9%
  去掉 price 和 rating                    6675704   5737308    1208518    1280003                  +5.9%
  参考:1000 个 price 的 8 字节 float64 二进制(拼接)zstd-3 后 3732 字节(原 8000 字节);同样 1000 个 price 的文本(逗号分隔)zstd-3 后 2619 字节(原 5921 字节)
  只看字符串字段(把某个字段的 2 万个值单独拿出来):
    title      JSON 数组  1223717 → zstd  413803;MsgPack 数组  1203160 → zstd  436587;换行分隔的纯文本  1183715 → zstd  409620
    url        JSON 数组  1135001 → zstd   11989;MsgPack 数组  1115003 → zstd   12386;换行分隔的纯文本  1094999 → zstd   12331
    updated_at JSON 数组   460001 → zstd  100143;MsgPack 数组   420003 → zstd   99373;换行分隔的纯文本   419999 → zstd   99436
    category   JSON 数组   451749 → zstd   35091;MsgPack 数组   411751 → zstd   35938;换行分隔的纯文本   411747 → zstd   34720

检验了两个猜想:浮点的 8 字节二进制压得差一些,但去掉浮点字段后 MessagePack 仍然大 5.9%,所以不是主因;长度不一的 title 字符串单独压缩时,MessagePack 数组比 JSON 数组大 5.5%,长度一致的 url 就没有差别。我的解释是字符串长度前缀是每条都要单独存的信息,而 JSON 的结束引号更好预测,这个解释没有拆到压缩器内部验证。

分步演示

MessagePack
本步的对象

    本文用到的工具与边界

    • 本机 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,取多次中位数,数字每次运行会有小幅抖动。msgpack 1.2.3、orjson 3.13.0 临时安装;数据集见第 1 篇;体积和类型是确定性结果,速度是 7 次中位数;第七节的字段单独压缩用 zstd 级别 3
    • 没有验证:CBOR、BSON 等其它二进制 JSON;MessagePack 的扩展类型(时间戳等);用 msgpack-numpy 之类处理数组;不同 zstd 级别下 products 的差距会不会变;为什么长度前缀更难压缩的压缩器内部原因(只在字段级别验证了现象);Python 以外的 MessagePack 实现
    京ICP备2026067502号-1京公网安备11010502063487号