利用msgspec加速大json文件反序列化速度

一个库的命运啊,当然要靠自我奋斗,但是也要考虑到项目的实际需求,有舍才有得。

内部项目有一个基础依赖的结果「下文简述为 origin.json.gz」是json.dump然后压缩成gz文件存储的,下游引用的时候有个头大的问题就是这玩意太大了,动辄百兆级别,一般的json.load就很慢,于是就一直在寻找可用的性能更为优秀的方法来替换掉内置json处理,至少期望是load快一些。

先后尝试过不同的json库,包括不限于以下选手:orjson/ujson/rapidjson/simplejson,基本的对比效果就是:序列化各种吊打内置库,但反序列化基本上没啥太大的优势,直到有一天老板甩了个分享链接:Faster, more memory-efficient Python JSON parsing with msgspec (pythonspeed.com)

发现msgspec这玩意简直强的离谱:提前定义好struct然后按需streaming加载的思路性能极佳且内存极度友好。

简单说明一下这个origin.json.gz的大致结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[
  {
    "A": ...一堆子子孙孙
    "B": ...二堆子子孙孙
    ...
    "Z": {
      "ZA": ...aaa
      "ZB": ...bbb
      ...
      "ZZ": ...zzz
    }
	},
  ...
]

某个下游引用的场景是:需要origin.json.gz里的某个节点的部分信息来做一些二次处理的事情。以往的操作不管如何先load一把,实在浪费资源。如果用msgspec来处理,是这样的姿势:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import gzip
from msgspec.json import decode
from msgspec import Struct


path = '...path...of...origin...json...gz...'

class Z(Struct):
    ZZ: List[dict]

class Item(Struct):
    Z: Dict[str, ZZ]

with gzip.open(path, 'rb') as fp:
    # 可以看到我只需要定义自己关心的节点链路即可,其他用不到的数据完全可以忽略不写
    # 这种读取方式必然是内存友好型了
    data = decode(fp.read(), type=List[Item])

for item in data:
    print(item.Z)

代码量看起来是比以前一把梭哈json.load多了一点,但收益巨大:同样的硬件条件,使用msgspec.decode快了近一个数量级。

虽然没有去翻源码去看具体实现,但二进制的世界没有魔法,无非就是在玩时间空间的把戏。msgspec.decode的快源于两点:

  1. 预定义了数据类型,他的核心解析器可以节省大量不必要的类型判断
  2. 按需定义,忽略不必要的数据

这就是一个空间换时间的玩法,按需加载显著降低了需要处理的数据量,自然性能就上来了。

这时候来了个成年人说我都要行不行?很遗憾,不行。在预定义了所有节点的数据结构「去掉按需加载的 buff」以后,msgspec.decode的速度甚至比内置json还慢 20% (基于一坨 400MB 大小的origin.json测试所得),object显然要比dict慢上一拍的。

# NOTE: I am not responsible for any expired content.
create@2022-05-25T08:12:46+08:00
update@2022-06-04T23:18:21+08:00
comment@https://github.com/ferstar/blog/issues/62
加载评论