我试图复制内存使用率测试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行)?
最佳答案
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 binary和small 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一致,尽管他们尚未更新发布的版本。
关于python - Python:无法复制内存使用情况的测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53543290/