Python:无法复制内存使用情况的测试

时间:2018-06-25 19:45:28

标签: python memory memory-management memory-profiling

我正在尝试复制内存使用情况测试here

从本质上讲,该帖子声称给出了以下代码段:

import copy
import memory_profiler

@profile
def function():
    x = list(range(1000000))  # allocate a big list
    y = copy.deepcopy(x)
    del x
    return y

if __name__ == "__main__":
    function()

调用

python -m memory_profiler memory-profile-me.py

在64位计算机上打印

Filename: memory-profile-me.py

Line #    Mem usage    Increment   Line Contents
================================================
 4                             @profile
 5      9.11 MB      0.00 MB   def function():
 6     40.05 MB     30.94 MB       x = list(range(1000000)) # allocate a big list
 7     89.73 MB     49.68 MB       y = copy.deepcopy(x)
 8     82.10 MB     -7.63 MB       del x
 9     82.10 MB      0.00 MB       return y

我复制并粘贴了相同的代码,但分析器产生了

Line #    Mem usage    Increment   Line Contents
================================================
 3   44.711 MiB   44.711 MiB   @profile
 4                             def function():
 5   83.309 MiB   38.598 MiB       x = list(range(1000000))  # allocate a big list
 6   90.793 MiB    7.484 MiB       y = copy.deepcopy(x)
 7   90.793 MiB    0.000 MiB       del x
 8   90.793 MiB    0.000 MiB       return y

此帖子可能已过时---探查程序包或python可能已更改。无论如何,我的问题是在Python 3.6.x中

(1)copy.deepcopy(x)(如上面的代码中所定义)是否应该消耗大量内存?

(2)为什么我不能复制?

(3)如果我在x = list(range(1000000))之后重复del x,那么内存增加的数量是否与我第一次分配x = list(range(1000000))的数量相同(如我的代码的第5行)? >

1 个答案:

答案 0 :(得分:5)

copy.deepcopy()仅递归复制可变对象,不复制诸如整数或字符串之类的不变对象。要复制的列表由不可变的整数组成,因此y副本最终共享对相同整数值的引用:

>>> import copy
>>> x = list(range(1000000))
>>> y = copy.deepcopy(x)
>>> x[-1] is y[-1]
True
>>> all(xv is yv for xv, yv in zip(x, y))
True

因此,副本仅需要创建一个具有100万个引用的新列表对象,该对象在Mac OS X 10.13(64位OS)上的Python 3.6上占用的内存超过8MB:

>>> import sys
>>> sys.getsizeof(y)
8697464
>>> sys.getsizeof(y) / 2 ** 20   # Mb
8.294548034667969

一个空的list对象占用64个字节,每个引用占用8个字节:

>>> sys.getsizeof([])
64
>>> sys.getsizeof([None])
72

Python列表对象总体分配了增长空间,将range()对象转换为列表会导致它为使用其他增长而留出的空间比使用deepcopy时要多一些,因此x略有增加仍然更大,在再次调整大小之前还有空间容纳125k对象:

>>> sys.getsizeof(x)
9000112
>>> sys.getsizeof(x) / 2 ** 20
8.583175659179688
>>> ((sys.getsizeof(x) - 64) // 8) - 10**6
125006

而副本只剩下大约87k的剩余空间:

>>> ((sys.getsizeof(y) - 64) // 8) - 10**6
87175

在Python 3.6上,我也不能重复这篇文章的声明,部分是因为Python在内存管理方面有了很多改进,部分是因为本文在某些方面是错误的。

copy.deepcopy()关于列表和整数的行为在copy.deepcopy()的悠久历史中从未改变过(见first revision of the module, added in 1995),并且对{内存数字是错误的,即使在Python 2.7上也是如此。

具体地说,我可以使用Python 2.7复制结果,这是我在计算机上看到的:

$ python -V
Python 2.7.15
$ python -m memory_profiler memtest.py
Filename: memtest.py

Line #    Mem usage    Increment   Line Contents
================================================
     4   28.406 MiB   28.406 MiB   @profile
     5                             def function():
     6   67.121 MiB   38.715 MiB       x = list(range(1000000))  # allocate a big list
     7  159.918 MiB   92.797 MiB       y = copy.deepcopy(x)
     8  159.918 MiB    0.000 MiB       del x
     9  159.918 MiB    0.000 MiB       return y

正在发生的事情是Python的内存管理系统正在分配新的内存块以进行额外的扩展。这并不是说新的y列表对象会占用将近93MiB的内存,而仅仅是OS分配给Python进程的额外内存,当该进程为对象堆请求更多内存时。列表对象本身要小很多

Python 3 tracemalloc module对于实际发生的情况更加准确:

python3 -m memory_profiler --backend tracemalloc memtest.py
Filename: memtest.py

Line #    Mem usage    Increment   Line Contents
================================================
     4    0.001 MiB    0.001 MiB   @profile
     5                             def function():
     6   35.280 MiB   35.279 MiB       x = list(range(1000000))  # allocate a big list
     7   35.281 MiB    0.001 MiB       y = copy.deepcopy(x)
     8   26.698 MiB   -8.583 MiB       del x
     9   26.698 MiB    0.000 MiB       return y

Python 3.x内存管理器和列表实现比2.7中的智能;显然,新列表对象能够放入创建x时预先分配的现有已有内存中。

我们可以使用manually built Python 2.7.12 tracemalloc binarysmall patch to memory_profile.py测试Python 2.7的行为。现在,我们在Python 2.7上也获得了更多令人放心的结果:

Filename: memtest.py

Line #    Mem usage    Increment   Line Contents
================================================
     4    0.099 MiB    0.099 MiB   @profile
     5                             def function():
     6   31.734 MiB   31.635 MiB       x = list(range(1000000))  # allocate a big list
     7   31.726 MiB   -0.008 MiB       y = copy.deepcopy(x)
     8   23.143 MiB   -8.583 MiB       del x
     9   23.141 MiB   -0.002 MiB       return y

我注意到作者也很困惑:

  

copy.deepcopy复制两个列表,并再次分配〜50 MB(我不确定50 MB-31 MB = 19 MB的额外开销来自何处

(加粗强调)。

这里的错误是假定Python进程大小中的所有内存更改都可以直接归因于特定对象,但是实际情况要复杂得多,因为内存管理器可以添加(并删除!)内存“ arenas”,这是为堆保留的内存块,如果需要的话,将在较大的块中这样做。这里的过程很复杂,因为它取决于interactions between Python's manager and the OS malloc implementation details。作者发现了一篇关于Python模型的较旧的文章,author of that article themselves has already tried to point this out被误认为是最新的。从Python 2.5开始,关于Python不释放内存的说法不再成立。

令人困扰的是,同样的误解导致作者建议不要使用pickle,但实际上,即使在Python 2上,该模块也很少添加簿记内存来跟踪递归结构。参见this gist for my testing methodology;在Python 2.7上使用cPickle会导致46MiB一次性增加(将create_file()调用加倍不会导致内存进一步增加)。在Python 3中,内存更改完全消失了。

我将与Theano团队就该帖子打开一个对话框,该文章是错误的,令人困惑的,而且Python 2.7很快就会被完全淘汰,因此他们确实应该专注于Python 3的内存模型。 (*)

当您从range()创建一个新列表而不是一个副本时,您会看到与第一次创建x类似的内存增加,因为您除了新的列表对象外,还要创建一组新的整数对象。除了a specific set of small integers之外,Python不会缓存和重复使用整数值进行range()操作。


(*) 附录:我在Thano项目中打开了issue #6619。该项目与我的评估和removed the page from their documentation一致,尽管他们尚未更新发布的版本。