4

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

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

我将感谢任何可以帮助我了解发生了什么的人。

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

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())。

4

3 回答 3

1

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

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

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

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

在释放容器和所有引用的对象(将 arenas 释放回系统)之后,这将返回一个int带有表达式的新对象result + 0。只要有对第一个结果对象的引用,堆就不能收缩。在这种情况下,当函数返回时,它会自动释放。

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

在 Python 3.3 中,这应该不是问题,因为对象分配器已修改为在可用的情况下对 arenas 使用匿名 mmap。(对象分配器的上限也被提高到 512 字节以适应 64 位平台,但这在这里无关紧要。)

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

于 2012-11-18T20:01:48.500 回答
0

我怀疑它会泄漏,我敢打赌只是垃圾收集还没有开始,所以使用的内存一直在增长。这是因为每一轮外循环,前一个当前列表都可以进行垃圾收集,但直到任何时候都不会被垃圾收集。

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

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

于 2012-11-17T16:08:50.640 回答
0

您没有内存泄漏。linux 上的进程在退出之前不会向操作系统释放内存。因此,您将在 eg 中看到的统计数据top只会上升。

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

于 2012-11-17T17:25:57.053 回答