加载pickle的Python对象占用大量内存
我有一个用Python生成的“pickle”对象,它的文件大小是180MB。当我把它解压时,内存使用量一下子飙升到2或3GB。你们有类似的经历吗?这正常吗?
这个对象是一个树形结构,里面包含一个字典:每条边是一个字母,每个节点是一个可能的单词。所以,要存储一个单词,你需要的边数和这个单词的长度是一样的。第一层最多有26个节点,第二层有26的平方,第三层有26的立方,依此类推。每个节点如果是一个单词,我会有一个属性指向关于这个单词的信息(比如动词、名词、定义等)。
我的单词最长大约有40个字符。我大约有50万个条目。到目前为止,一切都很好,直到我用简单的cpickle进行存储:生成了一个180MB的文件。 我在Mac OS上,当我解压这180MB时,系统给Python进程分配了2到3GB的“内存/虚拟内存” :(
我没有在这个树形结构中看到任何递归:边上有节点,而这些节点又有一个数组的数组。没有涉及递归。
我有点卡住了:加载这180MB的文件大约需要20秒(不说内存的问题)。我得说我的CPU速度不快:是一个1.3GHz的i5处理器。但我的硬盘是SSD。我只有4GB的内存。
为了把这50万个单词添加到我的树中,我读取了大约7000个文件,每个文件大约有100个单词。读取这些文件时,Mac OS分配的内存一下子涨到了15GB,主要是虚拟内存 :( 我一直在使用“with”语句来确保每个文件都能关闭,但这并没有太大帮助。读取一个40KB的文件大约需要0.2秒。对我来说,这似乎有点慢。把它添加到树中要快得多(0.002秒)。
最后,我想做一个对象数据库,但我觉得Python不太适合这个。也许我会考虑用MongoDB :(
class Trie():
"""
Class to store known entities / word / verbs...
"""
longest_word = -1
nb_entree = 0
def __init__(self):
self.children = {}
self.isWord = False
self.infos =[]
def add(self, orthographe, entree):
"""
Store a string with the given type and definition in the Trie structure.
"""
if len(orthographe) >Trie.longest_word:
Trie.longest_word = len(orthographe)
if len(orthographe)==0:
self.isWord = True
self.infos.append(entree)
Trie.nb_entree += 1
return True
car = orthographe[0]
if car not in self.children.keys():
self.children[car] = Trie()
self.children[car].add(orthographe[1:], entree)
2 个回答
你真的需要一次性把所有数据都加载到内存中吗?如果你并不需要所有数据,而只是想在某个时刻获取特定的部分,那么你可以考虑把字典映射到一组磁盘上的文件,而不是一个单独的文件……或者把字典映射到数据库表中。所以,如果你在寻找一种可以把大量数据字典保存到磁盘或数据库的方法,并且可以利用序列化和编码(比如编解码器和哈希表),那么你可以看看 klepto
。
klepto
提供了一种字典抽象,可以用来写入数据库,包括把你的文件系统当作数据库来使用(也就是说,可以把整个字典写入一个文件,或者把每个条目写入自己的文件)。对于大数据,我通常选择把字典表示为文件系统中的一个目录,每个条目对应一个文件。klepto
还提供了缓存算法,所以如果你使用文件系统作为字典的后端,可以通过利用内存缓存来避免一些速度上的损失。
>>> from klepto.archives import dir_archive
>>> d = {'a':1, 'b':2, 'c':map, 'd':None}
>>> # map a dict to a filesystem directory
>>> demo = dir_archive('demo', d, serialized=True)
>>> demo['a']
1
>>> demo['c']
<built-in function map>
>>> demo
dir_archive('demo', {'a': 1, 'c': <built-in function map>, 'b': 2, 'd': None}, cached=True)
>>> # is set to cache to memory, so use 'dump' to dump to the filesystem
>>> demo.dump()
>>> del demo
>>>
>>> demo = dir_archive('demo', {}, serialized=True)
>>> demo
dir_archive('demo', {}, cached=True)
>>> # demo is empty, load from disk
>>> demo.load()
>>> demo
dir_archive('demo', {'a': 1, 'c': <built-in function map>, 'b': 2, 'd': None}, cached=True)
>>> demo['c']
<built-in function map>
>>>
klepto
还有其他选项,比如 compression
和 memmode
,可以用来定制你的数据存储方式(例如,压缩级别、内存映射模式等)。使用(MySQL等)数据库作为后端和使用文件系统的接口是完全一样的。你也可以关闭内存缓存,这样每次读写都会直接操作存档,只需设置 cached=False
。
klepto
还提供了很多缓存算法(比如 mru
、lru
、lfu
等),帮助你管理内存中的缓存,并会使用这些算法来处理存档的读写。
你可以使用 cached=False
来完全关闭内存缓存,直接从磁盘或数据库读写。如果你的条目足够大,你可以选择写入磁盘,把每个条目放在自己的文件中。这里有一个同时做这两件事的例子。
>>> from klepto.archives import dir_archive
>>> # does not hold entries in memory, each entry will be stored on disk
>>> demo = dir_archive('demo', {}, serialized=True, cached=False)
>>> demo['a'] = 10
>>> demo['b'] = 20
>>> demo['c'] = min
>>> demo['d'] = [1,2,3]
不过,虽然这样可以大大减少加载时间,但可能会稍微降低整体执行速度……通常最好是指定内存缓存的最大容量,并选择一个好的缓存算法。你需要多尝试一下,找到适合你需求的平衡点。
在这里获取 klepto
: https://github.com/uqfoundation
在Python中,尤其是在64位的机器上,对象是非常大的。当我们把一个对象“打包”成文件时,它会变得很紧凑,适合存储在磁盘上。下面是一个打包后的示例:
>>> pickle.dumps({'x':'y','z':{'x':'y'}},-1)
'\x80\x02}q\x00(U\x01xq\x01U\x01yq\x02U\x01zq\x03}q\x04h\x01h\x02su.'
>>> pickletools.dis(_)
0: \x80 PROTO 2
2: } EMPTY_DICT
3: q BINPUT 0
5: ( MARK
6: U SHORT_BINSTRING 'x'
9: q BINPUT 1
11: U SHORT_BINSTRING 'y'
14: q BINPUT 2
16: U SHORT_BINSTRING 'z'
19: q BINPUT 3
21: } EMPTY_DICT
22: q BINPUT 4
24: h BINGET 1
26: h BINGET 2
28: s SETITEM
29: u SETITEMS (MARK at 5)
30: . STOP
可以看到,它的确很紧凑。如果可以的话,里面的内容不会重复。
但是在内存中,一个对象实际上包含了很多指针。我们来问问Python,一个空字典有多大(在64位机器上):
>>> {}.__sizeof__()
248
哇!一个空字典居然要248字节!注意,这个字典预留了最多可以放八个元素的空间。不过,即使字典里只有一个元素,你也得为这248字节买单。
一个类的实例会有一个字典来存放实例变量。你的Trie(字典树)还会有一个额外的字典来存放子节点。所以,每个实例大约要占用500字节。如果你有大约200万到400万个Trie对象,你就能很清楚地看到内存使用的来源了。
你可以通过给你的Trie添加一个__slots__
来稍微减轻这个问题,这样可以省去实例字典。这样做可能会节省大约750MB的内存(这是我的估计)。不过,这样会限制你不能再往Trie里添加更多的变量,但这可能不是个大问题。