加载的 pickle 数据在内存中比在磁盘上大得多并且似乎泄漏了。 (Python 2.7)

Loaded pickle data is much larger in memory than on disk and seems to leak. (Python 2.7)

我遇到了内存问题。我有一个用 Python 2.7 cPickle 模块编写的 pickle 文件。该文件在磁盘上有 2.2GB。它包含字典、列表和 numpy 数组的各种嵌套。

当我加载此文件时(再次在 Python 2.7 上使用 cPickle),python 进程最终使用了 5.13GB 内存。然后,如果我删除对加载数据的引用,则数据使用量会减少 2.79GB。程序最后还有2.38GB没有清理。

cPickle 是否在后端保留了一些缓存或记忆 table?这些额外的数据来自哪里?有办法清除吗?

加载的 cPickle 中没有自定义对象,只有字典、列表和 numpy 数组。我无法理解为什么它会这样。


令人反感的例子

这是我为演示该行为而编写的一个简单脚本:

from six.moves import cPickle as pickle
import time
import gc
import utool as ut

print('Create a memory tracker object to snapshot memory usage in the program')
memtrack = ut.MemoryTracker()

print('Print out how large the file is on disk')
fpath = 'tmp.pkl'
print(ut.get_file_nBytes_str('tmp.pkl'))

print('Report memory usage before loading the data')
memtrack.report()
print(' Load the data')
with open(fpath, 'rb') as file_:
    data = pickle.load(file_)

print(' Check how much data it used')
memtrack.report()

print(' Delete the reference and check again')
del data
memtrack.report()

print('Check to make sure the system doesnt want to clean itself up')

print(' This never does anything. I dont know why I bother')
time.sleep(1)
gc.collect()
memtrack.report()

time.sleep(10)
gc.collect()

for i in range(10000):
    time.sleep(.001)

print(' Check one more time')
memtrack.report()

这是它的输出

Create a memory tracker object to snapshot memory usage in the program
[memtrack] +----
[memtrack] | new MemoryTracker(Memtrack Init)
[memtrack] | Available Memory = 12.41 GB
[memtrack] | Used Memory      = 39.09 MB
[memtrack] L----
Print out how large the file is on disk
2.00 GB
Report memory usage before loading the data
[memtrack] +----
[memtrack] | diff(avail) = 0.00 KB
[memtrack] | [] diff(used) = 12.00 KB
[memtrack] | Available Memory = 12.41 GB
[memtrack] | Used Memory      = 39.11 MB
[memtrack] L----
 Load the data
 Check how much data it used
[memtrack] +----
[memtrack] | diff(avail) = 5.09 GB
[memtrack] | [] diff(used) = 5.13 GB
[memtrack] | Available Memory = 7.33 GB
[memtrack] | Used Memory      = 5.17 GB
[memtrack] L----
 Delete the reference and check again
[memtrack] +----
[memtrack] | diff(avail) = -2.80 GB
[memtrack] | [] diff(used) = -2.79 GB
[memtrack] | Available Memory = 10.12 GB
[memtrack] | Used Memory      = 2.38 GB
[memtrack] L----
Check to make sure the system doesnt want to clean itself up
 This never does anything. I dont know why I bother
[memtrack] +----
[memtrack] | diff(avail) = 40.00 KB
[memtrack] | [] diff(used) = 0.00 KB
[memtrack] | Available Memory = 10.12 GB
[memtrack] | Used Memory      = 2.38 GB
[memtrack] L----
 Check one more time
[memtrack] +----
[memtrack] | diff(avail) = -672.00 KB
[memtrack] | [] diff(used) = 0.00 KB
[memtrack] | Available Memory = 10.12 GB
[memtrack] | Used Memory      = 2.38 GB
[memtrack] L----

完整性检查 1(垃圾收集)

这里的健全性检查是分配相同数量的数据然后将其删除的脚本,进程会完美地自行清理。

这是脚本:

import numpy as np
import utool as ut

memtrack = ut.MemoryTracker()
data = np.empty(2200 * 2 ** 20, dtype=np.uint8) + 1
print(ut.byte_str2(data.nbytes))
memtrack.report()
del data
memtrack.report()

这是输出

[memtrack] +----
[memtrack] | new MemoryTracker(Memtrack Init)
[memtrack] | Available Memory = 12.34 GB
[memtrack] | Used Memory      = 39.08 MB
[memtrack] L----
2.15 GB
[memtrack] +----
[memtrack] | diff(avail) = 2.15 GB
[memtrack] | [] diff(used) = 2.15 GB
[memtrack] | Available Memory = 10.19 GB
[memtrack] | Used Memory      = 2.19 GB
[memtrack] L----
[memtrack] +----
[memtrack] | diff(avail) = -2.15 GB
[memtrack] | [] diff(used) = -2.15 GB
[memtrack] | Available Memory = 12.34 GB
[memtrack] | Used Memory      = 39.10 MB
[memtrack] L----

完整性检查 2(确保类型)

只是为了检查是否在此列表中没有自定义类型,这是此结构中出现的一组类型。 数据本身是一个具有以下键的字典: ['maws_lists', 'int_rvec', 'wx_lists', 'aid_to_idx', 'agg_flags', 'agg_rvecs', 'gamma_list', 'wx_to_idf', 'aids'、'fxs_lists'、'wx_to_aids']。 以下脚本特定于此结构的特定嵌套,但它详尽地显示了此容器中使用的类型:

print(data.keys())
type_set = set()
type_set.add(type(data['int_rvec']))
type_set.add(type(data['wx_to_aids']))
type_set.add(type(data['wx_to_idf']))
type_set.add(type(data['gamma_list']))
type_set.update(set([n2.dtype for n1 in  data['agg_flags'] for n2 in n1]))
type_set.update(set([n2.dtype for n1 in  data['agg_rvecs'] for n2 in n1]))
type_set.update(set([n2.dtype for n1 in  data['fxs_lists'] for n2 in n1]))
type_set.update(set([n2.dtype for n1 in  data['maws_lists'] for n2 in n1]))
type_set.update(set([n1.dtype for n1 in  data['wx_lists']]))
type_set.update(set([type(n1) for n1 in  data['aids']]))
type_set.update(set([type(n1) for n1 in  data['aid_to_idx'].keys()]))
type_set.update(set([type(n1) for n1 in  data['aid_to_idx'].values()]))

type set 的输出是

{bool,
 dtype('bool'),
 dtype('uint16'),
 dtype('int8'),
 dtype('int32'),
 dtype('float32'),
 NoneType,
 int}

这表明所有序列最终都解析为 None、标准 python 类型或标准 numpy 类型。你必须相信我,可迭代类型都是列表和字典。


简而言之,我的问题是:

这里的一个可能的罪魁祸首是 Python 通过设计过度分配数据结构,如列表和字典,以便更快地附加到它们,因为内存分配很慢。例如,在 32 位 Python 上,一个空字典有 36 个字节的 sys.getsizeof()。添加一个元素,它就变成了 52 个字节。它保持 52 字节,直到它有五个元素,此时它变成 68 字节。因此,很明显,当您附加第一个元素时,Python 为四个元素分配了足够的内存,然后当您添加第五个元素 (LEELOO DALLAS) 时,它为另外四个元素分配了足够的内存。随着列表的增长,添加的填充量增长得越来越快:本质上,每次填满列表时,您都会将列表的内存分配加倍。

所以我预计会有类似的事情发生,因为 pickle 协议似乎不存储 pickle 对象的长度,至少对于 Python 数据类型,所以它实际上是在读取一个列表或字典项目并附加它,并且 Python 正在增长对象,就像上面描述的那样添加项目。取决于对象的大小在您解开数据时如何摇晃,您的列表和词典中可能会留下很多额外的 space。 (但不确定 numpy 对象是如何存储的;它们可能更紧凑。)

可能还会分配一些临时对象,这将有助于解释内存使用量如何变得如此之大。

现在,当您制作列表或字典的副本时,Python 确切地知道它有多少项,并且可以为副本分配准确的内存量。如果一个假设的 5 元素列表 x 被分配了 68 个字节,因为它预计会增长到 8 个元素,那么副本 x[:] 被分配了 56 个字节,因为这是正确的数量。所以你可以在加载后用你的一个更大的物体试一试,看看它是否有明显的帮助。

但它可能不会。当对象被销毁时,Python 不一定将内存释放回 OS。相反,它可能会保留内存,以防它需要分配更多同类对象(这很可能),因为重用您已经拥有的内存比释放该内存只是为了稍后重新分配它的成本更低。因此,尽管 Python 可能没有将内存还给 OS,但这并不意味着存在泄漏。它可供脚本的其余部分使用,OS 只是看不到它。在这种情况下,没有办法强制 Python 归还它。

我不知道 utool 是什么(我找到了那个名字的 Python 包,但它似乎没有 MemoryTracker class)但取决于它测量的是什么,它可能会显示 OS 对它的看法,而不是 Python 的看法。在这种情况下,您看到的本质上是脚本的 peak 内存使用情况,因为 Python 正在保留该内存以防您需要它用于其他用途。如果你从不使用它,它最终会被 OS 换出,物理 RAM 将被提供给其他需要它的进程。

最重要的是,您的脚本使用的内存量本身并不是需要解决的问题,通常您也不需要关心。 (这就是您首先使用 Python 的原因!)您的脚本是否有效,并且 运行 是否足够快?那你就没事了。 Python 和 NumPy 都是成熟且广泛使用的软件;在像 pickle 库这样经常使用的东西中找到一个真正的、以前未检测到的这种大小的内存泄漏的可能性很小。

如果可用,将脚本的内存使用量与写入数据的脚本使用的内存量进行比较会很有趣。