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

try*_*lve 4 python memory memory-management memory-profiling

我试图在这里复制内存使用测试.

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

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

调用

python -m memory_profiler memory-profile-me.py
Run Code Online (Sandbox Code Playgroud)

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

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

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

这篇文章可能已经过时了 - 无论是profiler包还是python都可能已经改变了.无论如何,我的问题是,在Python 3.6.x中

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

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

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

Mar*_*ers 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
Run Code Online (Sandbox Code Playgroud)

因此,副本只需要创建一个包含100万个引用的新列表对象,这个对象在Mac OS X 10.13(64位操作系统)上的Python 3.6构建上占用8MB以上的内存:

>>> import sys
>>> sys.getsizeof(y)
8697464
>>> sys.getsizeof(y) / 2 ** 20   # Mb
8.294548034667969
Run Code Online (Sandbox Code Playgroud)

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

>>> sys.getsizeof([])
64
>>> sys.getsizeof([None])
72
Run Code Online (Sandbox Code Playgroud)

Python列表对象过度分配空间以进行增长,将range()对象转换为列表会使其为使用时的额外增长腾出更多空间deepcopy,因此x稍微大一点,在再次调整大小之前还有额外125k对象的空间:

>>> sys.getsizeof(x)
9000112
>>> sys.getsizeof(x) / 2 ** 20
8.583175659179688
>>> ((sys.getsizeof(x) - 64) // 8) - 10**6
125006
Run Code Online (Sandbox Code Playgroud)

虽然副本只有大约87k的剩余空间:

>>> ((sys.getsizeof(y) - 64) // 8) - 10**6
87175
Run Code Online (Sandbox Code Playgroud)

在Python 3.6上我也无法复制文章声明,部分原因是因为Python已经看到了很多内存管理改进,部分原因是文章在几个方面都是错误的.

copy.deepcopy()关于列表和整数的行为在历史悠久的历史中从未改变copy.deepcopy()(参见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
Run Code Online (Sandbox Code Playgroud)

发生的事情是Python的内存管理系统正在为新的扩展分配一大块内存.并不是新的y列表对象占用了近93MiB的内存,这只是当该进程为对象堆请求更多内存时操作系统分配给Python进程的额外内存.该列表对象本身是很多小.

Python 3的tracemalloc模块是一个很多关于到底发生了什么更准确:

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

Python 3.x内存管理器和列表实现比2.7中的那些更聪明; 显然,新的列表对象能够适应现有的已有内存,在创建时预先分配x.

我们可以使用手动构建的Python 2.7.12 tracemalloc二进制文件和一个小补丁来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
Run Code Online (Sandbox Code Playgroud)

我注意到作者也很困惑:

copy.deepcopy复制两个列表,再次分配~50 MB(我不知道50 MB的额外开销 - 31 MB = 19 MB来自哪里)

(大胆强调我的).

这里的错误是假设Python进程大小中的所有内存更改都可以直接归因于特定对象,但实际情况要复杂得多,因为内存管理器可以添加(并删除!)内存'arenas',内存块根据需要为堆保留,如果有意义,将在更大的块中保留.这里的过程很复杂,因为它取决于Python管理器和操作系统malloc实现细节之间的交互.作者发现了一篇关于Python模型的旧文章,他们误解了这篇文章是最新的,该文章作者自己已经试图指出这一点 ; 从Python 2.5开始,Python不释放内存的说法不再适用.

令人不安的是,同样的误解导致作者建议不要使用pickle,但实际上,即使在Python 2上,模块也不会添加超过一点的簿记内存来跟踪递归结构.请参阅此要点以了解我的测试方法 ; cPickle在Python 2.7上使用会增加一次46MiB的增加(create_file()调用结果加倍,不再增加内存).在Python 3中,内存的变化完全消失了.

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

当你创建一个新的列表,从range(),不是副本,你会看到在内存中有类似的增加作为创建x的第一次,因为你除了新的列表对象创建一组新的整数对象.除了一组特定的小整数外,Python不会为range()操作缓存和重用整数值.


(*) 附录:我用Thano项目打开了#6619号问题.该项目同意我的评估,并从他们的文档中删除了该页面,尽管他们尚未更新已发布的版本.