marshal 序列化更快,cPickle 反序列化更快

20 投票
6 回答
13898 浏览
提问于 2025-04-17 08:24

我正在实现一个需要对大对象进行序列化和反序列化的程序,所以我用 picklecPicklemarshal 模块做了一些测试,以便选择最合适的模块。在这个过程中,我发现了一些很有趣的事情:

我使用 dumps 方法,然后用 loads 方法(针对每个模块)对一个包含字典、元组、整数、浮点数和字符串的列表进行操作。

这是我基准测试的结果:

DUMPING a list of length 7340032
----------------------------------------------------------------------
pickle => 14.675 seconds
length of pickle serialized string: 31457430

cPickle => 2.619 seconds
length of cPickle serialized string: 31457457

marshal => 0.991 seconds
length of marshal serialized string: 117440540

LOADING a list of length: 7340032
----------------------------------------------------------------------
pickle => 13.768 seconds
(same length?) 7340032 == 7340032

cPickle => 2.038 seconds
(same length?) 7340032 == 7340032

marshal => 6.378 seconds
(same length?) 7340032 == 7340032

从这些结果中,我们可以看到 marshal 在基准测试的 导出 部分表现得非常快:

pickle 快了 14.8 倍,比 cPickle 快了 2.6 倍。

但令我非常惊讶的是,marshal导入 部分却远远慢于 cPickle

pickle 快了 2.2 倍,但比 cPickle 慢了 3.1 倍。

至于内存方面,marshal导入 时的表现也非常低效:

Ubuntu 系统监视器

我猜 marshal 导入速度慢的原因可能和它序列化后的字符串长度有关(比 picklecPickle 都要长得多)。

  • 为什么 marshal 导出快而导入慢?
  • 为什么 marshal 的序列化字符串这么长?
  • 为什么 marshal 的导入在内存中这么低效?
  • 有没有办法提高 marshal 的导入性能?
  • 有没有办法把 marshal 的快速导出和 cPickle 的快速导入结合起来?

6 个回答

10

这些基准测试之间的差异给我们提供了一个加速cPickle的思路:

Input: ["This is a string of 33 characters" for _ in xrange(1000000)]
cPickle dumps 0.199 s loads 0.099 s 2002041 bytes
marshal dumps 0.368 s loads 0.138 s 38000005 bytes

Input: ["This is a string of 33 "+"characters" for _ in xrange(1000000)]
cPickle dumps 1.374 s loads 0.550 s 40001244 bytes
marshal dumps 0.361 s loads 0.141 s 38000005 bytes

在第一个例子中,列表里重复了同一个字符串。而第二个列表虽然看起来一样,但每个字符串都是一个独立的对象,因为它们是通过某个表达式生成的。如果你最开始是从外部来源读取数据,那么你可以考虑进行一些字符串去重的操作。

17

有些人可能觉得这方法有点不太正规,但我用这个方法取得了很好的效果。具体来说,我在进行数据保存时,把调用的部分用gc.disable()和gc.enable()包裹起来。比如,下面的代码片段中,写入一个大约50MB的字典列表,从原来的78秒缩短到了4秒。

#  not a complete example....
gc.disable()
cPickle.dump(params,fout,cPickle.HIGHEST_PROTOCOL)         
fout.close()               
gc.enable()
21

cPickle 的算法比 marshal 更聪明,能够通过一些技巧来减少大对象所占用的空间。这意味着它在解码时会慢一些,但在编码时会快,因为生成的输出更小。

marshal 则比较简单,它直接把对象保存成原样,没有进行进一步的分析。这也解释了为什么 marshal 加载时效率低,因为它需要做更多的工作——也就是从磁盘读取更多的数据——才能做到和 cPickle 一样的事情。

最后,marshalcPickle 是完全不同的东西,你不能同时实现快速保存和快速加载,因为快速保存意味着对数据结构的分析少,这样就会把很多数据保存到磁盘上。

关于 marshal 可能与其他版本的 Python 不兼容的问题,通常建议使用 cPickle

“这不是一个通用的‘持久化’模块。对于 Python 对象的通用持久化和通过 RPC 调用进行传输,请查看模块 pickle 和 shelve。marshal 模块主要用于支持读取和写入 Python 模块的‘伪编译’代码,也就是 .pyc 文件。因此,Python 的维护者保留在必要时以向后不兼容的方式修改 marshal 格式的权利。如果你要序列化和反序列化 Python 对象,请使用 pickle 模块——性能相当,版本独立性有保障,并且 pickle 支持的对象范围远远超过 marshal。” (关于 marshal 的 Python 文档)

撰写回答