为什么一个版本泄漏内存但不泄漏另一个版本? (蟒蛇)

时间:2012-11-17 15:46:07

标签: python python-3.x

这两个函数以基本相同的方式计算相同的事物(整数的数量,使得相关的Collat​​z序列的长度不大于n)。唯一的区别是第一个使用集合,而第二个使用集合和列表。

第二个泄漏内存(至少在IDLE中使用Python 3.2),第一个没有,我不知道为什么。我尝试了一些“技巧”(例如添加del语句)但似乎没有任何帮助(这并不奇怪,那些技巧应该无用)。

我会感激任何可以帮助我理解发生了什么的人。

如果你想测试代码,你可能应该在55到65范围内使用n的值,任何高于75的值几乎肯定会导致(完全预期的)内存错误。< / em>的

def disk(n):
    """Uses sets for explored, current and to_explore. Does not leak."""
    explored = set()
    current = {1}
    for i in range(n):
        to_explore = set()
        for x in current:
            if not (x-1) % 3 and ((x-1)//3) % 2 and not ((x-1)//3) in explored:
                to_explore.add((x-1)//3)
            if not 2*x in explored:
                to_explore.add(2*x)
        explored.update(current)
        current = to_explore
    return len(explored)

def disk_2(n):
    """Does exactly the same thing, but Uses a set for explored and lists for
        current and to_explore. 
       Leaks (like a sieve :))
    """
    explored = set()
    current = [1]
    for i in range(n):
        to_explore = []
        for x in current:
            if not (x-1) % 3 and ((x-1)//3) % 2 and not ((x-1)//3) in explored:
                to_explore.append((x-1)//3)
            if not 2*x in explored:
                to_explore.append(2*x)
        explored.update(current)
        current = to_explore
    return len(explored)

编辑:当使用解释器的交互模式(没有IDLE)时也会发生这种情况,但是当直接从终端运行脚本时却没有(在这种情况下,内存使用率恢复正常)函数返回后的时间,或者只要显式调用gc.collect())。

3 个答案:

答案 0 :(得分:1)

CPython allocates small objects(obmalloc.c,3.2.3)在4个KiB池中,它在256个KiB块中管理,称为竞技场。每个活动池具有固定块大小,范围从8字节到256字节,步长为8.例如,从具有16字节块大小的第一个可用池分配14字节对象。

如果在堆上分配竞技场而不是使用mmap(这可以通过mallopt's M_MMAP_THRESHOLD进行调整),则存在潜在的问题,因为堆不能缩小到最高分配的竞技场以下,这将不会被释放只要1个池中的1个块被分配给一个对象(CPython不会在内存中浮动对象)。

鉴于上述情况,您的函数的以下版本应该可以解决问题。将行return len(explored)替换为以下3行:

    result = len(explored)
    del i, x, to_explore, current, explored
    return result + 0

取消分配容器和所有引用的对象(将竞技场释放回系统)后,将返回一个带有int表达式的新result + 0。只要存在对第一个结果对象的引用,堆就无法收缩。在这种情况下,当函数返回时会自动解除分配。

如果您在没有&#34;加0&#34;的情况下进行交互式测试。请记住,REPL(读取,评估,打印,循环)保留对通过伪变量&#34; _&#34;可访问的最后结果的引用。

在Python 3.3中,这不应该是一个问题,因为对象分配器被修改为use anonymous mmap for arenas,如果可用的话。 (对象分配器的上限也被提升到512字节以容纳64位平台,但这在这里是无关紧要的。)

关于手动垃圾收集,gc.collect()执行跟踪容器对象的完整集合,但它也由内置类型(例如,框架,方法,浮点数)维护的对象clears freelists。 Python 3.3添加了额外的API函数来清除列表(PyList_ClearFreeList),dicts(PyDict_ClearFreeList)和集合(PySet_ClearFreeList)使用的空闲列表。如果您希望保持空闲列表不变,请使用gc.collect(1)

答案 1 :(得分:0)

我怀疑它是否泄漏,我敢打赌它只是垃圾收集还没有开始,因此使用的内存不断增长。这是因为每一轮外循环,前一个当前列表都变得易于垃圾收集,但是直到每次都不会被垃圾收集。

此外,即使是垃圾回收,内存通常也不会释放回操作系统,因此您必须使用任何Python方法来获取当前使用的堆大小。

如果在每个外部循环迭代结束时添加垃圾收集,那么可能会减少内存使用量,这取决于Python在没有它的情况下如何处理其堆和垃圾收集。

答案 2 :(得分:0)

您没有内存泄漏。 Linux上的进程在退出之前不会向操作系统释放内存。因此,您将在例如top只会上升。

如果在运行相同或更小的作业后,只有内存泄漏,Python会从操作系统中获取更多内存,当它“应该”能够重用它用于“应该”的对象的内存时已被垃圾收集。