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

时间:2016-09-20 01:05:34

标签: python python-2.7 numpy pickle

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

当我加载这个文件时(再次使用Python 2.7上的cPickle),python进程最终使用5.13GB的内存。然后,如果我删除对加载数据的引用,则数据使用量将下降2.79GB。在程序结束时,还有另外2.38GB尚未清理。

cPickle是否在后端保留了一些缓存或memoization表?这些额外数据来自哪里?有没有办法清除它?

加载的cPickle中没有自定义对象,只有dicts,lists和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','帮助',' 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()]))

类型集的输出是

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

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

简而言之,我的问题是:

  • 为什么加载一个2GB的pickle文件最终会在RAM中使用5GB的内存?
  • 为什么在最近加载的数据被垃圾回收时只清理2.5GB / 5GB?
  • 有什么方法可以收回这个丢失的记忆吗?

1 个答案:

答案 0 :(得分:6)

这里可能的一个罪魁祸首是,根据设计,Python会对列表和字典之类的数据结构进行全面分配,以便更快地附加到它们,因为内存分配很慢。例如,在32位Python上,空字典的sys.getsizeof()为36个字节。添加一个元素,它变为52个字节。它保持52个字节,直到它有5个元素,此时它变为68个字节。所以,很明显,当你附加第一个元素时,Python为4分配了足够的内存,然后在你添加第五个元素(LEELOO DALLAS)时为它分配了足够的内存。随着列表的增长,添加的填充量增长越来越快:基本上,每次填充时,列表的内存分配都会增加一倍。

所以我希望有类似的东西,因为pickle协议似乎不存储pickle对象的长度,至少对于Python数据类型,所以它实际上是一次读取一个列表或字典项并附加它,并且Python正在增长对象,因为项目的添加正如上所述。根据取消数据时对象大小的抖动方式,您的列表和词典中可能会留下大量额外空间。 (但不确定numpy对象是如何存储的;它们可能更紧凑。)

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

现在,当您复制列表或字典时,Python确切地知道它有多少项,并且可以为该副本分配恰当数量的内存。如果假设的5个元素列表x被分配68个字节,因为它预计将增长到8个元素,则复制x[:]被分配56个字节,因为这是正确的数量。因此,您可以在加载后用一个更大的对象进行拍摄,看看它是否有明显的帮助。

但它可能不会。当对象被销毁时,Python不一定会将内存释放回操作系统。相反,它可以保留在内存中,以防它需要分配更多相同类型的对象(这很可能),因为重用已经拥有的内存比释放该内存仅稍后重新分配它的成本更低。因此,虽然Python可能没有将内存返回给操作系统,但这并不意味着存在泄漏。它可供其余脚本使用,操作系统无法看到它。在这种情况下,没有办法强迫Python回复它。

我不知道utool是什么(我发现这个名字的Python包但它似乎没有MemoryTracker类)但是根据它的测量结果,它可能是显示操作系统对它的看法,而不是Python的。在这种情况下,您所看到的实际上是您的脚本的峰值内存使用,因为Python会保留该内存,以防您需要其它内容。如果你从不使用它,它最终将被操作系统换掉,物理RAM将被提供给其他需要它的进程。

总而言之,您的脚本使用的内存量本身并不是一个需要解决的问题,而且一般情况下您不需要关注自己。 (这就是你首先使用Python的原因!)你的脚本是否有效,并且运行得足够快?那你很好。 Python和NumPy都是成熟且广泛使用的软件;在pickle库经常使用的东西中发现这种大小的真正的,以前未被发现的内存泄漏的可能性非常小。

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