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

Ero*_*mic 2 python numpy pickle python-2.7

我有记忆问题.我有一个用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()
Run Code Online (Sandbox Code Playgroud)

这是它的输出

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----
Run Code Online (Sandbox Code Playgroud)

理智检查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()
Run Code Online (Sandbox Code Playgroud)

这是输出

[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----
Run Code Online (Sandbox Code Playgroud)

完善性检查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()]))
Run Code Online (Sandbox Code Playgroud)

类型集的输出是

{bool,
 dtype('bool'),
 dtype('uint16'),
 dtype('int8'),
 dtype('int32'),
 dtype('float32'),
 NoneType,
 int}
Run Code Online (Sandbox Code Playgroud)

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


简而言之,我的问题是:

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

kin*_*all 6

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

因此,我希望有这样的事情怎么回事,因为咸菜协议不会出现存储腌制对象的长度,至少对Python的数据类型,所以它基本上是读一次在一个列表或字典项并将其附加,正如上面所描述的那样添加项目时,Python正在增长对象.根据取消数据时对象大小的抖动方式,您的列表和词典中可能会留下大量额外空间.(但不确定numpy对象是如何存储的;它们可能更紧凑.)

潜在地也有一些临时对象被分配,这将有助于解释内存使用量如何变大.

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

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

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

最重要的是,脚本使用的内存量本身并不是一个需要解决的问题,而且通常不需要关注自己.(这就是你首先使用Python的原因!)你的脚本是否有效,并且运行得足够快?那你很好.Python和NumPy都是成熟且广泛使用的软件; 在pickle图书馆经常使用的东西中找到这种尺寸的真正的,以前未被发现的内存泄漏的可能性非常小.

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

  • 作为后续,我测试了两件事。删除数据然后重新加载它并加载数据两次。(1) 删除数据并重新加载它会导致相同的 5GB 内存使用。这表明当对象失去其引用时,所有数据都真正释放回 Python;Python 只是不会将该内存返回给操作系统(这很好)。(2) 两次加载数据导致使用 10GB。这表明 python 列表被过度分配,2GB 文件在加载 pickle 时确实使用了 5GB 内存。仍然需要测试复制数据。 (2认同)