为什么 protobuf 在内存中比 python 中的普通 dict+list 小?

Why protobuf is smaller in memory than normal dict+list in python?

我在嵌套 dict/list 中有一个大型原始类型结构。结构比较复杂,无所谓。

如果我用 python 的内置类型表示它 (dict/list/float/int/str) 它需要 1.1 GB,但如果我将它存储在 protobuf 中并将其加载到内存中,它会小得多。总计约 250 MB。

我想知道怎么会这样。与某些外部库相比,python 中的内置类型是否效率低下?

编辑:结构是从 json 文件加载的。所以对象之间没有内部引用

这很正常,这完全是 space 与时间的权衡。内存布局取决于特定数据结构的实现方式,而这又取决于它的使用方式。

A​​ general-purpose 字典通常使用哈希表实现。它有一个存储 key-value 对的存储桶列表 fixed-size。字典中的项目数可以小于、等于或大于桶的数量。如果更小,space 就浪费了。如果更大,字典操作需要很长时间。哈希表实现通常从一个小的初始桶列表开始,然后随着新项目的添加而增长,以保持良好的性能。但是,调整大小还需要重新散列,这在计算上非常昂贵,因此无论何时执行此操作,您都希望留出一些增长空间。 General-purpose 字典是 trade-off 介于 space 和时间之间,因为它们“不知道”它们应该包含多少元素,也因为没有完美的散列函数。但在 good-enough 情况下,general-use 哈希表将为您提供 near-O(1) 性能。

当数据被序列化时,情况就不同了。传输中的数据不会改变,您不会对其进行查找,它不会受到垃圾收集、边界对齐等的影响。这意味着您可以简单地一个接一个地打包键和值以提高 space 效率。只要可以重建值,您几乎不需要元数据和控制结构。不利的一面是,操作打包数据非常慢,因为所有操作都需要 O(n) 时间。

出于这个原因,您几乎总是想要:

  • 在发送之前将数据从 time-efficient 转换为 space-efficient 格式
  • 收到数据后,将space-efficient数据转换成time-efficient格式。

如果您使用的是嵌套词典(或列表,它们在很多方面都很相似),差异会加起来并变得更加明显。当你提前知道item的个数,数据变化不大的时候,通过为它预分配内存,大概可以得到一些改善,比如dict.fromkeys(range(count)).

“简单”python 对象,例如 intfloat,需要比 protobuf 使用的 C-counterparts 更多的内存。

让我们以 list 的 Python 整数为例,与整数数组进行比较,例如 array.array(即 array.array('i', ...))。

array.array 的分析很简单:从 array.arrays 对象中丢弃一些开销,每个元素只需要 4 个字节(C-integer 的大小)。

整数列表的情况完全不同:

  • 该列表包含的不是 integer-objects 本身,而是指向对象的指针(8 64 位 execu 的附加字节 table)
  • 即使是一个小的non-zero整数也至少需要28字节(参见import sys; sys.getsizeof(1)returns28):引用计数需要8个字节,8个字节用于保存指向 integer-function table 的指针,整数值的大小需要 8 个字节(Python 的整数可以比 2^32 大得多),并且至少需要 4 个字节保存整数值本身。
  • 还有一个

这意味着与可能的 4 字节(如果我们使用 long long int,即 64 位整数,则为 8 字节)相比,每个 Python 整数的成本高达 40.5 字节。

doubles(即 array.array('d',...))的数组相比,具有 Python 浮点数的列表的情况类似,每个元素只需要大约 8 个字节。但是对于列表我们有:

  • 该列表不包含浮点对象本身,而是指向对象的指针(8 64 位 execu 的附加字节table)
  • 一个float对象需要24字节(参见import sys; sys.getsizeof(1.0)returns24):引用计数需要8个字节,8个字节用来保存指向float-function table,以及 8 个字节来保存 double-值本身。
  • 因为 24 是 8 的倍数,内存管理的开销“仅”约为 0.5 字节。

这意味着 Python 浮点对象为 32.5 字节,而 C-double.

为 8 字节

protobuf 在内部使用与 array.array 相同的数据表示,因此需要更少的内存(如您所见,大约少 4-5 倍)。 numpy.array 是数据类型的另一个示例,它包含原始数据 C-values,因此需要的内存比列表少得多。


如果不需要在字典中搜索,那么将 key-values-pairs 保存在列表中将比在字典中需要更少的内存,因为不必维护用于搜索的结构 (这会增加一些内存成本) - 这也是导致 protobuf-data.

内存占用量较小的另一件事

回答你的另一个问题:没有 built-in 模块到 Python-dictarray.array 到 Python-list,所以我借这个机会厚颜无耻地plug-in为我的一个图书馆做广告:cykhash.

cykhash need less than 25% 的 Python'S-dict/set 内存中设置和映射,但速度大致相同。