【问题标题】:Reducing memory used by a large dict减少大字典使用的内存
【发布时间】:2013-09-04 13:19:13
【问题描述】:

我需要创建一个内存对象,该对象具有 9 位整数的键和与每个键关联的布尔值。我一直在使用 dict,如下面的简化示例所示:

#!/usr/bin/python
from __future__ import print_function
import sys

myDict = {}
for n in range(56000):
        myDict[n] = True

print('Count:',len(myDict),' Size:', sys.getsizeof(myDict))

我需要能够查找和检索与每个键关联的布尔值。问题是字典的大小。在 64 位 Linux 系统上使用 Python 2.7 和上述示例,根据 sys.getsizeof(),dict 的大小为 3.1 兆字节。 (每个条目大约 56 个字节来存储 9 个数字和一个布尔值)

我需要在字典中存储(大约)55.000 个条目的布尔状态。每个 dict 键是一个 9 位整数。我尝试使用整数和 str(theInteger) 作为键,而字典的大小没有变化。

我应该使用其他类型的数据结构或方法来节省如此庞大的数据集的内存吗?

【问题讨论】:

  • 您真的对三态(TrueFalse,字典中没有键)感兴趣,还是只对布尔值感兴趣?在后一种情况下,set 仅用于 True 值更合适。

标签: python python-2.7


【解决方案1】:

如果“未找到键”对您来说不是一个重要的状态(即您可以将不在数组中的键视为False),您可以使用set 来仅存储映射到@ 的元素987654323@。这需要减少大约 30% 的空间,因为每个条目仅包含两个 64 位量(散列和键),而不是三个量(散列、键、值)。

请记住,sys.getsizeof(dict) 只告诉您dict 本身的大小,而不是其中包含的对象。创建 56000 ints 作为键也将承担其自身的成本:每个整数 24 字节(类型指针、引用计数、值)。除了字典占用的内存之外,它本身将达到 1.3MB。

要真正节省空间,您可以使用 NumPy 压缩稀疏行 矩阵:

from scipy.sparse import lil_matrix # linked-list matrix, used to construct the csr matrix
vals = lil_matrix((1,1000000000), dtype='int8'))
# We will use 0 = no such key, 1 = True, 2 = False
for n in myIndices:
    vals[n] = 1
vals = vals.tocsr()

vals 的内存使用量非常小:数据 56KB,索引 224KB,其他结构不到 1KB。因此,总大小小于 281KB(比 dict 小 10 倍),没有额外分配的整数。查找元素和更改非零元素非常快(在已排序的数组中进行二进制搜索),但插入新的非零值或将现有的非零值归零是昂贵的操作。

【讨论】:

    【解决方案2】:

    如果您使用整数键查找布尔值,并且键的范围从 0 开始并且是连续的,那么确实没有理由使用列表:

    my_list = []
    for n in range(56000):
            my_list[n] = True
    

    或更好:

    my_list = [True for n in range(5600])
    

    如果这还不够,请尝试array 模块并使用每个布尔值一个字节:

    import array
    my_array = array.array("b", (True for n in range(56000)))
    

    如果这还不够好,请尝试bitarray module on PyPi

    另一个想法是使用set:假设您的FalseTrue 多,只需设置一组即可:

    my_true_numbers = {0, 2323, 23452} # just the True ones
    

    并检查

    value = number in my_true_numbers
    

    如果您的TrueFalse 多,则反之。

    【讨论】:

    • 集合的想法是一个很好的想法,取决于分布(True 多于 False 还是相反)集合中的值可以表示 True 或 False。以最好的为准。
    • 感谢 Christian 和其他做出贡献的人。我会试试Christian的set方法。看起来很有希望。
    • 要列出 56000 True 使用 [ True ] * 56000。但是请注意,相同的引用重复了 56000 次,但因为 True 是不可变的,所以没关系。
    【解决方案3】:

    Python: Reducing memory usage of dictionary 的公认答案得出的结论是,你无能为力,我同意。字典的整体内存开销很小,但您的示例中键值对的数量会增加内存占用。

    可能有人认为:如果键始终是线性的,您可以直接创建一个布尔值列表,或者更好地使用bitarray。然后,密钥将是隐式的。但是,如果这只是在您的示例中,您将无能为力。

    【讨论】:

    • 感谢您的建议。不幸的是,键不是线性的。问题是,即使只有 56,000 个条目,每个条目键都是从 100,000,000 到 800,000,000 的某个整数,所以我认为我的列表或位数组需要 800,000,000 个元素长才能直接访问非键对象中的布尔值。跨度>
    • 还有一点证据让我相信,有更多的东西可以控制内存分配,而不是看起来。如果我的 dict 使用大约 12,000 字节,那么当添加下一个条目时,dict 的大小会跳转到大约 48,000 字节。然后它会在 48,000 停留一段时间,然后突然跳到 192,000 字节等,每次扩展时都会增加 4 倍于之前的大小。
    • 可能是某种优化以防止每个新条目分配内存。大多数列表...实现都是这样做的。
    • Christian Schramm 的想法似乎值得一试。在最坏的情况下,您需要两套。一个代表真,一个代表假,但您不需要存储布尔值。
    • @tea2code:那太贵了。与字典相比,set 仅节省 30% 的空间;拥有两个集合将需要比单个字典更多的空间(因为 set 过度分配)。
    【解决方案4】:

    为什么不使用巨大的位域? 由于您至少需要三个值:true、false 和 not_init/UB,因此您将数据编码为两位。使用的总内存为55.000*2 bits = 110 000 bits = 13 kBytes

    此处设置标志是为了确保该值已被用户正确设置(不是必需的),第二位包含该值。

    使用64 bit unsigned integers,您只需要其中的203 来存储整个数组。

    然后您可以使用位索引访问它:假设您要访问索引123 处的值。您将需要访问位 #246#247(一个用于设置布尔值,另一个用于值)。

    由于246247 不如2**64,所以它们存储在first uint 上。要访问它们:

    return (( (1<<246) & array[0] ) >> 246 )
    

    访问任何位:

    return (( (1<<n) & array[ n/(2**64) ] ) >> n)
    

    (未测试位访问器)

    设置一点:

    array[ n/(2**64) ] = array[ n/(2**64) ] | (1<<n)
    

    按位运算很棘手(算术移位与逻辑运算)并且不易调试,但它们可能非常强大。

    【讨论】:

      【解决方案5】:

      根据您的具体需求,您可以使用列表来存储您的值。这将只使用字典使用的大约 16% 的空间,但某些操作(例如查找和插入)会(可能很多)慢。

      values = list(range(56000))
      

      如果您使用 bisect 模块并将您的值存储在排序列表中,您的查找仍然会比使用 dict 慢,但比幼稚的 x in my_list 检查快得多。

      列表必须始终保持排序。要检查一个值是否在你的列表中,你可以使用这个函数:

      def is_in_list(values, x):
          i = bisect_left(values, x)
          return i != len(values) and values[i] == x
      

      像这样工作:

      >>> is_in_list([2, 4, 14, 15], 4)
      True
      >>> is_in_list([2, 4, 14, 15], 1)
      False
      >>> is_in_list([2, 4, 14, 15], 13)
      False
      

      此方法将显着减少内存使用量,但 – compared to a dict or set – 查找需要 O(log n) 时间而不是 O(1),插入需要 O(n) 而不是 O(1)。

      【讨论】:

        猜你喜欢
        • 2012-05-03
        • 1970-01-01
        • 1970-01-01
        • 2022-06-12
        • 1970-01-01
        • 2013-05-21
        • 2013-11-19
        • 1970-01-01
        • 2022-08-23
        相关资源
        最近更新 更多