调试numpy.load()的内存泄漏

1473阅读 0评论2012-02-24 图片MM
分类:

内存泄漏的程序

下面是再现内存泄漏的程序,运行此程序之后,可以观察到进程的内存使用量一直上升。

import numpy as np

# 创建一个测试用的文件
X = np.random.randn(1000,1000)
np.savez('tmp.npz',X=X)

# 循环重复载入此文件
for i in xrange(1000000):
    data = np.load('tmp.npz')
    data.close()
统计内存中的对象

为了弄清楚是什么对象没有被垃圾回收,我们可以在循环多次之后,调用gc模块的get_objects()获得当前内存中的所有对象,并对这些对象的类型进行计数统计。下面是这部分程序:

for i in xrange(10000):
    data = np.load('tmp.npz')
    data.close()

import gc
from collections import defaultdict

d = defaultdict(int) #使用defaultdict对对象进行计数
for o in gc.get_objects():
    name = type(o).__name__ #获得对象的类型名
    d[name] += 1

items = d.items()
items.sort(key=lambda x:x[1])
for key, value in items:
    print key, value

这段程序使用defaultdict对内存中的对象进行计数,键为对象的类型名,值为对象的个数。最后我们按照个数从小到大排列并输出。下面是对象个数最多的几种类型:

...
function 2416
tuple 9440
BagObj 10000
NpzFile 10000
list 20297
dict 21032

由于程序循环了10000次,显然BagObj和NpzFile由load()产生,但是未能被垃圾回收的对象。

查看NumPy源程序

知道了产生泄漏的类名,我们可以在NumPy的安装文件夹下搜索着两个字符串,在我的电脑上找到:

c:\Python26\Lib\site-packages\numpy\lib\npyio.py

分析此文件中的BagObj和NpzFile的定义:

class NpzFile(object):
    def __init__(self, fid, own_fid=False):
        ...
        self.zip = _zip
        self.f = BagObj(self)
        if own_fid:
            self.fid = fid
        else:
            self.fid = None

    def close(self):
        """
        Close the file.

        """
        if self.zip is not None:
            self.zip.close()
            self.zip = None
        if self.fid is not None:
            self.fid.close()
            self.fid = None

    def __del__(self):
        self.close()

class BagObj(object):
    def __init__(self, obj):
        self._obj = obj    
    def __getattribute__(self, key):
        try:
            return object.__getattribute__(self, '_obj')[key]
        except KeyError:
            raise AttributeError, key

我们发现NpzFile类定义了__del__(),有此方法的类特别需要注意内存泄漏问题。NpzFile的f属性是一个BagObj对象,而此BagObj对象的_obj属性是NpzFile对象。这样在NpzFile和BagObj对象之间存在循环引用。

在gc模块的文档中特别提到过循环引用并存在__del__()时的内存释放问题:

gc模块中关于循环引用和__del__()的说明

为了解决这个问题,我们需要打破循环引用。对于本问题来说,有两种方法:

for i in xrange(1000000):
    data = np.load('tmp.npz')
    del data.f        
    #del data.f._obj
    data.close()

删除NpzFile对BagObj的引用。这时BagObj对象将会被先回收,从而使得NpzFile对象的引用次数为0,调用其__del__(),并回收NpzFile对象。

删除BagObj对NpzFile的引用。这时NpzFile对象的引用次数为0,调用其__del__(),回收NpzFile对象之后,BagObj对象的引用次数变为0,回收。

从上面的分析可知,根据打破循环引用的方式不同,__del__()调用的时刻也不同。当循环引用中的各个对象没有__del__()时,对象回收的顺序不会造成任何影响。如果有对象包含__del__(),垃圾回收器就犯难了,它无法擅自决定__del__()的调用时刻,于是它选择了让这些对象始终在内存中呆着。

如果让你修复这个BUG,你会如何修改NumPy的源程序?
上一篇:初学者漫谈C++ 之一
下一篇:arm中断的理解