【问题标题】:efficient way to hold and process a big dict in memory in python在python中保存和处理内存中的大字典的有效方法
【发布时间】:2013-08-05 04:43:53
【问题描述】:

当我做了一些测试时,一个包含 3000 万个项目的 int=>int(不同值)的 python dict 很容易在我的 mac 上吃掉 >2G 的内存。由于我只使用 int 到 int dict,有没有比使用 python dict 更好的解决方案?

我需要的一些要求是,

  1. 在保存数千万级别的 int 到 int 项时内存效率更高
  2. 基本的 dict 方法,例如按键获取值和迭代所有项目
  3. 易于序列化为字符串/二进制将是一个加分项

更新, 4. 通过给定的键轻松获取子集,例如 d.fromkeys([...])

谢谢。

【问题讨论】:

  • Python 对象非常大,但我认为它们不足以将 3000 万个整数对的字典增加到 2 GB。我期望更多的几百兆字节。你是如何确定这些数字的?您使用的是 64 位 Python,还是您的整数特别大(> 数十亿)?
  • @delnan, @Srika\ Appal,这是一个简单的字典,例如 {1:30000001, 2:30000002, ..., 30000000:60000000}。不太现实,但我只是为了测试目的而创建它。我只是在 macbook 64 位、python 2.7.5 上使用“for i in range(30000000): d[i]=i+30000000”,而没有显式调用任何 GC。经过双重测试,它使用了 3.06G :)
  • @The-IT,如果有一些基于 C 的库,它有一个 python 接口并且可以很容易地与我现有的 python 逻辑相结合,那就太好了。 :)
  • 刚刚在32位Python上做了测试;它是 1.46GB。显然,大量的ints 是 64 位 Python 严重失败的领域。
  • @JasonHsu:粗略计算表明,使用 32 位整数的哈希表自行滚动实现将只有 300MB(在简单的 [int, int] 对,负载因子为 0.8)。您可以在 array 之上轻松实现它,或者在 C 中实现它以获得原始性能。如果实施得当,专门针对您的应用程序调整的数据结构肯定会胜过任何通用容器。

标签: python dictionary


【解决方案1】:

至少有两种可能:

数组

您可以尝试使用两个数组。一个用于键,一个用于值,因此 index(key) == index(value)

2017-01-05 更新:在数组中使用 4 字节整数。

数组会使用更少的内存。在使用 clang 编译 python 的 64 位 FreeBSD 机器上,包含 3000 万个整数的数组使用大约 117 MiB。

这些是我使用的python命令:

Python 2.7.13 (default, Dec 28 2016, 20:51:25) 
[GCC 4.2.1 Compatible FreeBSD Clang 3.8.0 (tags/RELEASE_380/final 262564)] on freebsd11
Type "help", "copyright", "credits" or "license" for more information.
>>> from array import array
>>> a = array('i', xrange(30000000))
>>> a.itemsize
4

导入数组后,ps 报告:

USER     PID %CPU %MEM   VSZ  RSS TT  STAT STARTED    TIME COMMAND
 rsmith 81023  0.0  0.2  35480   8100  0  I+   20:35     0:00.03 python (python2.7)

制作数组后:

USER     PID %CPU %MEM    VSZ    RSS TT  STAT STARTED    TIME COMMAND
rsmith 81023 29.0  3.1 168600 128776  0  S+   20:35     0:04.52 python (python2.7)

驻留集大小以 1 KiB 为单位报告,因此 (128776 - 8100)/1024 = 117 MiB

使用列表推导,您可以轻松获得键满足特定条件的索引列表。然后,您可以使用该列表中的索引来访问相应的值...

numpy

如果你有可用的 numpy,那么使用它会更快、有更多的功能并且使用的 RAM 会稍微少一些:

Python 2.7.5 (default, Jun 10 2013, 19:54:11) 
[GCC 4.2.1 Compatible FreeBSD Clang 3.1 ((branches/release_31 156863))] on freebsd9
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> a = np.arange(0, 30000000, dtype=np.int32)

来自ps:启动 Python 后为 6700 KiB,导入 numpy 后为 17400 KiB,创建数组后为 134824 KiB。这大约是 114 MiB。

此外,numpy 支持record arrays

Python 2.7.5 (default, Jun 10 2013, 19:54:11) 
[GCC 4.2.1 Compatible FreeBSD Clang 3.1 ((branches/release_31 156863))] on freebsd9
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> a = np.zeros((10,), dtype=('i4,i4'))
>>> a
array([(0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0),
       (0, 0), (0, 0)], 
      dtype=[('f0', '<i4'), ('f1', '<i4')])
>>> a.dtype.names
('f0', 'f1')
>>> a.dtype.names = ('key', 'value')
>>> a
array([(0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0),
       (0, 0), (0, 0)], 
      dtype=[('key', '<i4'), ('value', '<i4')])
>>> a[3] = (12, 5429)
>>> a
array([(0, 0), (0, 0), (0, 0), (12, 5429), (0, 0), (0, 0), (0, 0), (0, 0),
       (0, 0), (0, 0)], 
      dtype=[('key', '<i4'), ('value', '<i4')])
>>> a[3]['key']
12

这里可以分别访问键和值;

>>> a['key']
array([ 0,  0,  0, 12,  0,  0,  0,  0,  0,  0], dtype=int32)

【讨论】:

  • 感谢您的建议,我错过了一些关键要求,即 k-v 搜索、通过给定键获取子集仍然很重要。所以我不能简单地将它们存储到 2 个数组中。
  • @JasonHsu:那么 numpy 记录数组怎么样?
  • 我将首先尝试一些基于 Judy-array 的解决方案,如下面的回答,如果不行,那么回到尝试 Numpy,因为 ~O(1) 查找时间对我来说仍然很重要。感谢您的信息。 :)
  • 这是一个令人难以置信的答案,应该得到比它更多的支持。
  • 您对array.array 不公平,因为您将64 位整数数组与32 位整数np.array 进行比较。对于大多数 64 位系统,“l”表示 64 位有符号整数。您可以使用 a=array.array('l') 检查项目大小,然后使用 a.itemsize 最有可能是 8。np.array 仍然是一个更好的选择,因为有更多开箱即用的功能。
【解决方案2】:

基于 Judy-array 的解决方案似乎是我应该研究的选项。我仍在寻找可以被 Python 使用的良好实现。稍后会更新。

更新,

最后,我在 http://code.google.com/p/py-judy/ 试验了一个 Judy 数组包装器。 那里似乎没有任何文档,但我试图通过 dir(...) 它的包和对象简单地找到它的方法,但是它可以工作。

使用 judy.JudyIntObjectMap 在相同的实验中,它在标准字典的 1/3 处吃掉了 986MB。它还提供了 JudyIntSet,在某些特殊情况下会节省更多内存,因为与 JudyIntObjectMap 相比,它不需要引用任何真正的 Python 对象作为值。

(如下进一步测试,JudyArray 只使用了几 MB 到几十 MB,其中大部分 ~986MB 实际上是由 Python 内存空间中的值对象使用的。)

如果对你有帮助,这里有一些代码,

>>> import judy
>>> dir(judy)
['JudyIntObjectMap', 'JudyIntSet', '__doc__', '__file__', '__name__', '__package__']
>>> a=judy.JudyIntObjectMap()
>>> dir(a)
['__class__', '__contains__', '__delattr__', '__delitem__', '__doc__', '__format__', '__getattribute__', '__getitem__', '__hash__', '__init__', '__iter__', '__len__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__value_sizeof__', 'by_index', 'clear', 'get', 'iteritems', 'iterkeys', 'itervalues', 'pop']
>>> a[100]=1
>>> a[100]="str"
>>> a["str"]="str"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'non-integer keys not supported'
>>> for i in xrange(30000000):
...     a[i]=i+30000000   #finally eats ~986MB memory
... 

更新,

好的,经过测试的 30M int 的 JudyIntSet。

>>> a=judy.JudyIntSet()
>>> a.add(1111111111111111111111111)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: we only support integers in the range [0, 2**64-1]

它完全只使用 5.7MB 来存储 30M 顺序 int 数组 [0,30000000),这可能是由于 JudyArray 的自动压缩。 709MB 以上是 bcz 我使用 range(...) 而不是更合适的 xrange(...) 来生成数据。

所以核心 JudyArray 的大小与 30M int 是完全可以忽略的。

如果有人知道更完整的 Judy Array 包装器实现,请告诉我,因为此包装器仅包装 JudyIntObjectMap 和 JudyIntSet。对于 int-int dict,JudyIntObjectMap 仍然需要真正的 python 对象。如果我们只对值进行 counter_add 和设置,那么将值的 int 存储在 C 空间中而不是使用 python 对象将是一个好主意。希望有人有兴趣创建或介绍一个:)

【讨论】:

    【解决方案3】:

    如果您想要的只是一个易于使用的类似字典的计数器,则添加另一个答案。

    High performance Counter object from Python standard library

    【讨论】:

      【解决方案4】:

      如果我们对如何使用它有更多了解,可能会更容易提出好的解决方案。 您说您想通过键获取值并遍历所有值,但没有关于是否需要插入/删除数据。

      一种非常有效的数据存储方式是使用array 模块。如果您不需要插入/删除数据,您可以简单地拥有两个数组。 “键”数组将被排序,您可以对正确的键进行二进制搜索。然后你只需从另一个数组中的相同位置选择值。

      您可以轻松地将其封装在行为类似于 dict 的类中。我不知道某处是否有现成的解决方案,但实施起来应该不会非常困难。这应该可以帮助您避免使用大量消耗内存的 python 对象。

      但您可能有其他要求使这种解决方案不切实际/不可能。

      【讨论】:

      • 感谢您的建议。我仍然需要通过给定的键集获取大字典的子集,例如 d.fromkeys([...])。可以只扫描和过滤键数组,并在插入时防止重复...所以数组不是我的选择。
      猜你喜欢
      • 2011-07-12
      • 2014-03-30
      • 1970-01-01
      • 2017-04-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多