加载大型字典占用的内存空间巨大

35

我在硬盘上有一个只有168MB的文件。它是一个逗号分隔的单词和ID列表。单词可以是1-5个字符长。共有650万行。

我在Python中创建了一个字典,将其加载到内存中,以便可以将传入的文本与该单词列表进行比较。当Python将其加载到内存中时,会显示使用了1.3GB的RAM空间。有什么想法吗?

假设我的单词文件看起来像这样...

1,word1
2,word2
3,word3

然后再加上650万。

然后我通过循环遍历该文件并创建一个字典(Python 2.6.1):

def load_term_cache():
    """will load the term cache from our cached file instead of hitting mysql. If it didn't
    preload into memory it would be 20+ million queries per process"""
    global cached_terms
    dumpfile = os.path.join(os.getenv("MY_PATH"), 'datafiles', 'baseterms.txt')
    f = open(dumpfile)
    cache = csv.reader(f)
    for term_id, term in cache:
        cached_terms[term] = term_id
    f.close()

仅仅这样做就会使内存占用量激增。我查看了活动监视器,发现它把内存使用最大化,高达约1.5GB的RAM。在我的笔记本电脑上,它只是开始交换空间。有什么办法可以在Python中最有效地存储键值对到内存中吗?

更新:我尝试使用anydb模块,在加载了440万条记录后,它就挂掉了。浮点数是自从我尝试加载它以来经过的秒数。

56.95
3400018
60.12
3600019
63.27
3800020
66.43
4000021
69.59
4200022
72.75
4400023
83.42
4600024
168.61
4800025
338.57

你可以看到它一开始运行得很好。每几秒钟插入20万行,直到我遇到瓶颈,时间加倍。
import anydbm

i=0
mark=0
starttime = time.time()
dbfile = os.path.join(os.getenv("MY_PATH"), 'datafiles', 'baseterms')
db = anydbm.open(dbfile, 'c')
#load from existing baseterm file
termfile = os.path.join(os.getenv("MY_PATH"), 'datafiles', 'baseterms.txt.LARGE')
for line in open(termfile):
    i += 1
    pieces = line.split(',')
    db[str(pieces[1])] = str(pieces[0])
    if i > mark:
        print i
        print round(time.time() - starttime, 2)
        mark = i + 200000
db.close()
4个回答

40
很多想法。但如果你需要实际帮助,请编辑你的问题以展示你所有的代码。同时告诉我们“it”是什么,它在加载零条目文件时显示了什么,你所在的平台是什么,Python的版本是什么。
你说“单词可以由1-5个单词组成”。关键字段的平均长度是多少字节?ID都是整数吗?如果是整数,最小和最大整数是什么?如果不是,ID的平均长度是多少字节?为了使上述所有内容可以互相核对,你的650万行文件有多少字节?
看着你的代码,一个1行文件word1,1将创建一个dict d ['1'] =' word1' ...这难道不是反过来的吗?
更新3:更多问题:如何编码“word”?你确定两个字段中没有任何一个载入尾随空格吗?
更新4...你问“如何在Python中最有效地存储键/值对”,“没有人用任何准确性回答过这个问题”。
你有一个大小为168MB的文件,其中有650万行。那是每行27.1字节(假设它是*x平台),去掉逗号和换行符,每行剩下25个字节。假设“id”旨在是唯一的,并且似乎是整数,让我们假设“id”长度为7个字节;那就留下了18个字节的“word”的平均大小。这符合你的期望吗?
所以,我们想在内存中存储一个18字节的键和一个7字节的值的查找表。
让我们假设是32位的CPython 2.6平台。
>>> K = sys.getsizeof('123456789012345678')
>>> V = sys.getsizeof('1234567')
>>> K, V
(42, 31)

请注意,sys.getsizeof(str_object) => 24 + len(str_object)

有一个回答者提到了元组。请仔细注意以下内容:

>>> sys.getsizeof(())
28
>>> sys.getsizeof((1,))
32
>>> sys.getsizeof((1,2))
36
>>> sys.getsizeof((1,2,3))
40
>>> sys.getsizeof(("foo", "bar"))
36
>>> sys.getsizeof(("fooooooooooooooooooooooo", "bar"))
36
>>>
结论:sys.getsizeof(tuple_object) => 28 + 4 * len(tuple_object) ... 它仅允许对每个项目使用指针,不允许对项目的大小进行计算。
类似地,对于列表的分析显示:sys.getsizeof(list_object) => 36 + 4 * len(list_object) ... 再次需要添加项目的大小。还有一个进一步的考虑:CPython会过度分配列表,以便它不必在每个list.append()调用上调用系统realloc()。对于足够大的尺寸(如650万!),过度分配为12.5%-请参见源代码(Objects/listobject.c)。这种过度分配不会在元组中进行(它们的大小不会改变)。
以下是用于基于内存的查找表的各种替代方案的成本: 元组列表: 每个元组本身将占用36字节,加上K和V的内容。因此,N个元组将需要N *(36 + K + V);然后您需要一个列表来保存它们,因此我们需要36 + 1.125 * 4 * N。
元组列表的总计:36 + N *(40.5 + K + v)
当N为650万时,大约为709 MB。 两个平行列表: (36 + 1.125 * 4 * N + K * N)+(36 + 1.125 * 4 * N + V * N) 即72 + N *(9 + K + V)
请注意,当N为650万时,40.5 * N和9 * N之间的差异约为200MB。 将值存储为int而不是str: 但这还不是全部。如果ID实际上是整数,我们可以将它们作为整数存储。
>>> sys.getsizeof(1234567)
12

每个值对象的大小由31个字节减少到了12个字节。当N为650万时,这差异是19*N,又可以节省约118MB。

对于整数值,请使用array.array('l')代替列表:

我们可以将那些7位数字的整数存储在array.array('l')中。没有int对象,也没有指向它们的指针--只有一个4字节的有符号整数值。奖励: 数组只超额分配6.25%(对于大的N)。因此,现在是1.0625 * 4 * N,而不是先前的(1.125 * 4 + 12) * N, 再节省12.25 * N,即76 MB。

所以,我们缩减到了709-200-118-76=约315 MB

N.B. 错误和遗漏除外——在我的时区是0127 :-(


嘿,约翰,我已经进行了编辑。希望这能帮助展示我做了什么愚蠢的事情 :) - James
抱歉,我写错了,我已经编辑了一下以展示数据在磁盘1上的存储方式。 - James
是的,在使用wordstr.strip()保存之前,所有内容都已被剥离。 - James
@beagleguy:请贴出你实际运行的代码,最好是复制/粘贴。并回答其他问题。 - John Machin
那段代码是实际运行以加载术语文件的代码。 - James
@beagleguy:请通过编辑您的问题来回答问题,而不是在评论中回答。 - John Machin

21

看一下(Python 2.6,32位版本)...:

>>> sys.getsizeof('word,1')
30
>>> sys.getsizeof(('word', '1'))
36
>>> sys.getsizeof(dict(word='1'))
140

字符串(在磁盘上占用6个字节)会有24个字节的开销(无论它有多长,都需要将其长度加上24以找出它占用多少内存)。 将其拆分为元组会稍微多一点。 但是真正导致内存飙升的是dict:即使是空字典也需要140个字节 - 这是维护基于哈希查找的纯开销。为了保持快速,哈希表必须具有低密度 - Python通过为其占用大量额外内存来确保dict始终具有低密度。

存储键/值对最节省内存的方法是作为元组列表,但是查找当然会非常慢(即使您对列表进行排序并使用bisect进行查找,它仍然比字典要慢得多)。

考虑改用shelve - 这将使用很少的内存(因为数据驻留在磁盘上),同时仍然提供相当不错的查找性能(当然不如内存中的字典快,但对于大量数据,它比在元组列表上进行的查找要快得多,即使是排序的元组列表也无法比拟!)。


感谢Alex提供的一些有用信息,我现在正在尝试使用shelve库,之后会告诉你它的效果如何。 - James
hrm shelve不行,只能处理500万行数据就开始崩溃了,最终我在20分钟后放弃了插入操作。 - James
@beagleguy:你试过使用protocol=-1的shelve吗? - John Machin
"protocol=-1" 是一个不错的建议,但我不知道 "started dying" 是什么意思——可能只是垃圾回收行为在耗尽物理内存后导致操作缓慢。所以下一步要尝试的是真正的数据库,无论是关系型还是非关系型——bsddb 是其中之一(非关系型),并且被 anydbm(和间接地 shelve)隐式使用,但是 Python 自带的版本不是最好或最快的。我建议先尝试 Python 自带的 sqlite,以避免安装任何其他组件;如果仍然不够,可以考虑使用第三方数据库。 - Alex Martelli

8

将您的数据转换为dbm(导入anydbm,或通过导入bsddb使用berkerley db ...),然后使用dbm API访问它。

爆炸的原因是Python对任何对象都有额外的元信息,而字典需要构造哈希表(这需要更多的内存)。您刚刚创建了太多对象(6.5M),因此元数据变得太大。

import bsddb
a = bsddb.btopen('a.bdb') # you can also try bsddb.hashopen
for x in xrange(10500) :
  a['word%d' %x] = '%d' %x
a.close()

这段代码只需要1秒钟就能运行完,所以我认为速度还可以(因为你说每秒10500行)。btopen创建的db文件长度为499,712字节,而hashopen创建的长度为319,488字节。

使用xrange输入6.5M并使用btopen时,我得到了417,080KB的输出文件大小,并且插入完成大约需要1到2分钟。因此,我认为它完全适合您的需求。


1
你不需要SQL数据库(它们功能强大但速度较慢)。DBM是哈希值风格的数据库,因此它们应该很快(非常类似于字典对象),并且需要比SQL更小的内存占用。Berkeley db将是我的最佳选择(导入bsddb)。 - Francis
2
@beagleguy:dbm风格的数据库是基于键值的,而不是SQL。现在你已经让它在字典上工作了,修改程序使其能够与其中一个一起工作并不需要太多时间(相对于从MySQL转到字典),然后你可以与MySQL进行比较。这总比让1.3G的RAM被你的数据占用要好 :) - Ricardo Cárdenes
1
嗨Heim,今晚我实际上尝试使用Redis来完成这个目的。插入数据花了10分钟的时间。我平均每秒插入10,500次,比redis基准测试少了10倍。我使用的是Python Redis客户端模块。此外,相同的168MB文件,Redis占用了约1GB的内存。 - James
请查看我的编辑 - 使用btopen会创建一个400M的文件数据库,并且在我的电脑上只需要1或2分钟即可完成。 - Francis
1
好的,我认为你不需要担心3.0,除非你非常决定现在使用3.0。shelve更像是Python本地存储,因此它应该工作类似,但性能不能保证。 - Francis
显示剩余7条评论

0

虽然我来晚了,但我也遇到了同样的问题。其他人已经很好地回答了这个问题。我提供一个易于使用(也许不是那么容易 :-) )而且相当高效的替代方案,那就是pandas.DataFrame。它在保存大量数据时内存使用效率很高。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接