我正在尝试复制内存使用情况测试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行)? >
答案 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 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一致,尽管他们尚未更新发布的版本。