加载pickle的Python对象占用大量内存

7 投票
2 回答
8151 浏览
提问于 2025-04-18 17:17

我有一个用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 个回答

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 还有其他选项,比如 compressionmemmode,可以用来定制你的数据存储方式(例如,压缩级别、内存映射模式等)。使用(MySQL等)数据库作为后端和使用文件系统的接口是完全一样的。你也可以关闭内存缓存,这样每次读写都会直接操作存档,只需设置 cached=False

klepto 还提供了很多缓存算法(比如 mrulrulfu 等),帮助你管理内存中的缓存,并会使用这些算法来处理存档的读写。

你可以使用 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]

不过,虽然这样可以大大减少加载时间,但可能会稍微降低整体执行速度……通常最好是指定内存缓存的最大容量,并选择一个好的缓存算法。你需要多尝试一下,找到适合你需求的平衡点。

在这里获取 kleptohttps://github.com/uqfoundation

4

在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里添加更多的变量,但这可能不是个大问题。

撰写回答