许多词典使用大量的RAM

时间:2017-04-24 02:25:13

标签: python python-3.x dictionary memory optimization

我有一个非常简单的Python脚本来创建(用于测试目的),列表中有3500万个字典对象。每个字典对象包含两个键/值对。例如

{'Name': 'Jordan', 'Age': 35}

该脚本非常简单地对名称和年龄进行查询,搜索字典列表并返回包含所有匹配字典条目索引的新列表。

但是,正如您在下面看到的那样,消耗了大量内存。我认为我在某个地方犯了一个非常天真的错误。

screenshot of code and task manager show ram usage

我的代码如下:(如果更具可读性,也可以在图像中查看)。

import sys

# Firstly, we will create 35 million records in memory, all will be the same apart from one

def search(key, value, data, age):
    print("Searching, please wait")
    # Create list to store returned PKs
    foundPKS = []
    for index in range(0, len(data)):
        if key in data[index] and 'Age' in data[index]:
            if data[index][key] == value and data[index]['Age'] >= age:
                foundPKS.append(index)
    results = foundPKS
    return results

def createdata():
    # Let's create our list for storing our dictionaries
    print("Creating database, please wait")
    dictList = []
    for index in range(0, 35000000):
        # Define dictionary
        record = {'Name': 'Jordan', 'Age': 25}
        if 24500123 <= index <= 24500200:
            record['Name'] = 'Chris'
            record['Age'] = 33
        # Add the dict to a list
        dictList.append(record)
    return dictList

datareturned = createdata()

keyname = input("For which key do you wish to search?")
valuename = input("Which values do you want to find?")
valueage = input("What is the minimum age?")

print("Full data set object size:" + str(sys.getsizeof(datareturned)))
results = search(keyname, valuename, datareturned, int(valueage))

if len(results) > 0:
    print(str(len(results)) + " found. Writing to results.txt")
    fo = open("results.txt", "w")
    for line in range(0, len(results)):
        fo.write(str(results[line]) + "\n")
    fo.close()

导致RAM大量消耗的原因是什么?

4 个答案:

答案 0 :(得分:13)

dict对象的开销非常大。这取决于您的Python版本和系统架构,但取决于Python 3.5 64位

In [21]: sys.getsizeof({})
Out[21]: 288

所以猜测:

250*36e6*1e-9 == 9.0

如果我创建了很多词典,那么这是我在千兆字节中使用ram的下限,而不是考虑list

不是使用dict作为记录类型,而不是用例,请使用namedtuple

为了了解这是如何比较的,让我们设置一个等效的元组列表:

In [23]: Record = namedtuple("Record", "name age")

In [24]: records = [Record("john", 28) for _ in range(36000000)]

In [25]: getsizeof = sys.getsizeof

考虑:

In [31]: sum(getsizeof(record)+ getsizeof(record.name) + getsizeof(record.age)  for record in records)
Out[31]: 5220000000

In [32]: _ + getsizeof(records)
Out[32]: 5517842208

In [33]: _ * 1e-9
Out[33]: 5.517842208

所以5演出是一个相当保守的上限。例如,它假定没有小型int缓存,对于年龄的记录类型将完全重要。在我自己的系统上,python进程正在注册2.7 gig的内存使用量(通过top)。

所以,在我的机器上实际发生的事情更好地建模为保守字符串假设 - 平均大小为10的唯一字符串,因此没有字符串实习 - 但对于int是自由的,假设int-caching是为我们处理我们的int对象,所以我们只需要担心8字节的指针!

In [35]: sum(getsizeof("0123456789") + 8  for record in records)
Out[35]: 2412000000

In [36]: _ + getsizeof(records)
Out[36]: 2709842208

In [37]: _ * 1e-9
Out[37]: 2.709842208

对于我从top观察的内容,这是一个很好的模型。

如果你真的想要高效存储

现在,如果你真的想把数据塞入ram,那么你将不得不失去Python的灵活性。您可以将array模块与struct结合使用,以获得类似C的内存效率。一个更容易涉及的世界可能是numpy,这允许类似的事情。例如:

In [1]: import numpy as np

In [2]: recordtype = np.dtype([('name', 'S20'),('age', np.uint8)])

In [3]: records = np.empty((36000000), dtype=recordtype)

In [4]: records.nbytes
Out[4]: 756000000

In [5]: records.nbytes*1e-9
Out[5]: 0.756

请注意,我们现在可以非常紧凑。我可以使用8位无符号整数(即单个字节)来表示年龄。但是,我立即面临一些不灵活性:如果我想要有效存储字符串,我必须定义最大尺寸。我使用了'S20',即20个字符。这些是ASCII字节,但是20个ascii字符的字段可能足以满足名称。

现在,numpy为您提供了许多包装C编译代码的快速方法。因此,只是为了解决它,让我们用一些玩具数据填充我们的记录。名称只是简单计数的数字串,年龄将从正态分布中选择,平均值为50,标准差为10.

In [8]: for i in range(1, 36000000+1):
   ...:     records['name'][i - 1] = b"%08d" % i
   ...:

In [9]: import random
   ...: for i in range(36000000):
   ...:     records['age'][i] = max(0, int(random.normalvariate(50, 10)))
   ...:

现在,我们可以使用numpy来查询records。例如,如果您希望记录的索引具有某些条件,请使用np.where

In [10]: np.where(records['age'] > 70)
Out[10]: (array([      58,      146,      192, ..., 35999635, 35999768, 35999927]),)

In [11]: idx = np.where(records['age'] > 70)[0]

In [12]: len(idx)
Out[12]: 643403

所有643403记录的年龄为> 70。现在,让我们试试100

In [13]: idx = np.where(records['age'] > 100)[0]

In [14]: len(idx)
Out[14]: 9

In [15]: idx
Out[15]:
array([ 2315458,  5088296,  5161049,  7079762, 15574072, 17995993,
       25665975, 26724665, 28322943])

In [16]: records[idx]
Out[16]:
array([(b'02315459', 101), (b'05088297', 102), (b'05161050', 101),
       (b'07079763', 104), (b'15574073', 101), (b'17995994', 102),
       (b'25665976', 101), (b'26724666', 102), (b'28322944', 101)],
      dtype=[('name', 'S20'), ('age', 'u1')])

当然,一个主要的不灵活性是numpy数组大小。调整大小的操作很昂贵。现在,您可以将numpy.array包装在某个类中,它将作为一个有效的主干,但在这一点上,您也可以使用真正的数据库。幸运的是,Python附带sqlite

答案 1 :(得分:2)

让我们来看看

>>> import sys 
>>> sys.getsizeof({'Name': 'Jordan', 'Age': 25}) * 35000000
10080000000

所以~10 GB。 Python正在做你要求它做的事情。

您需要将其拆分为chucks并按顺序检查它们。尝试this作为起点

答案 2 :(得分:0)

  

...列表中有3500万个字典对象。每个字典对象   包含两个键/值对。例如。 {'姓名':'乔丹','年龄':35}

这种存储方式的开销很大,你说得对。

Flyweight Design Pattern表明解决方案涉及分解共性。以下是对相同数据的替代存储以及更好的空间利用率的两个想法。

您可以使用__slots__来节省类实例的空间(这会抑制每个实例字典的创建):

class Person(object):
    __slots__ = ['Name', 'Age']

s = [Person('Jordan', 35), Person('Martin', 31), Person('Mary', 33)]

使用像一对并行列表这样的密集数据结构更节省空间:

s_name = ['Jordan', 'Martin', 'Mary']
s_age = [35, 31, 33]

如果数据中有重复项,则interning值可以节省更多空间:

s_name = map(intern, s_name)

或者在Python 3中:

s_name = list(map(sys.intern, s_name)

答案 3 :(得分:0)

有数百万个小对象的紧凑方法,需要比 __slots__ 更少的内存。

pip3 install recordclass

from recordclass import dataobject

class Person(dataobject):
   name:str
   age:int

每个实例为python节省16个字节>=3.8,为python<3.8节省24个字节