python - 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 行)?
解决方案
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()
x
range()
(*) 附录:我在 Thano 项目中打开了issue #6619 。该项目同意我的评估并从他们的文档中删除了该页面,尽管他们尚未更新已发布的版本。
推荐阅读
- reactjs - 带有文本字段输入的扩展面板在输入焦点上更改面板的背景颜色
- c# - Unity 窗口标题和元素文本不显示 (KSP)
- angular - 有没有办法在不使用结构指令的情况下显示动态选择的组件
- python - PyQt5 中的 mssql 数据类型更改为浮点数和日期更改 =PyQt5.QtCore.QDateTime(yyyy, mm, dd, 0, 0)
- linux - 使用 QEMU 进行 Beaglebone 黑色仿真
- javascript - useEffect 的清理函数中的变量值是否保持上次运行 useEffect 时的值?
- laravel - Laravel webserver css 和 js 文件没有被反映
- javascript - 数据变化随机更新 - Vue/Vuex
- r - R 和 SPSS 返回不同的参数以进行对数线性分析
- sqlite - 如何从 sqlite 选择多个输出