首页 > 解决方案 > Python:无法复制关于内存使用的测试

问题描述

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

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

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) 为什么我不能复制?

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

标签: pythonmemorymemory-managementmemory-profiling

解决方案


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 位操作系统)的 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()(参见模块的第一个修订版,于 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 的内存,这只是当 Python 进程为对象堆请求更多内存时操作系统分配给 Python 进程的额外内存。列表对象本身要小得多

Python 3tracemalloc模块对实际发生的情况要准确得多:

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

我们可以使用手动构建的 Python 2.7.12 tracemalloc 二进制文件和. 现在我们在 Python 2.7 上也得到了更令人放心的结果:memory_profile.py

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 进程大小的所有内存变化都可以直接归因于特定对象,但现实要复杂得多,因为内存管理器可以添加(和删除!)内存“竞技场”,内存块根据需要为堆保留,如果有意义的话,将在更大的块中这样做。这里的过程很复杂,因为它取决于Python 的管理器和操作系统malloc实现细节之间的交互。作者发现了一篇关于 Python 模型的旧文章,他们误解为是最新的,那篇文章的作者自己已经试图指出这一点;从 Python 2.5 开始,关于 Python 不释放内存的说法不再正确。

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

我将与 Theano 团队就这篇文章展开对话,这篇文章是错误的、令人困惑的,而且 Python 2.7 很快就会完全过时,所以他们真的应该关注 Python 3 的内存模型。(*)

当您从而不是副本创建新列表时,您会看到与第一次创建时类似的内存增加,因为除了新列表对象之外,您还将创建一组新的整数对象。除了一组特定的小整数之外,Python 不会缓存和重用整数值进行操作。range()xrange()


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


推荐阅读