Python: 无法复制内存使用测试
Python: Cannot replicate a test on memory usage
我正在尝试复制内存使用测试 here。
本质上,post 声明给出了以下代码片段:
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
此 post 可能已过时 --- 探查器包或 python 可能已更改。无论如何,我的问题是 Python 3.6.x
(1) copy.deepcopy(x)
(如上面代码中定义的)是否应该消耗大量内存?
(2) 为什么我无法复制?
(3) 如果我在 del x
之后重复 x = list(range(1000000))
,内存会增加与我第一次分配 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 万个引用的新列表对象,这个对象在我的 Python 3.6 build on Mac [=126= 上占用了 8MB 多一点的内存] X 10.13(64 位OS):
>>> 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 列表对象过度分配 space 增长,将 range()
对象转换为列表会导致它比使用时多 space 额外增长deepcopy
,因此 x
仍然稍大一些,在必须再次调整大小之前有空间容纳额外的 125k 个对象:
>>> sys.getsizeof(x)
9000112
>>> sys.getsizeof(x) / 2 ** 20
8.583175659179688
>>> ((sys.getsizeof(x) - 64) // 8) - 10**6
125006
而副本只有额外的 space 剩余约 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 and a 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
copies both lists, which allocates again ~50 MB (I am not sure where the additional overhead of 50 MB - 31 MB = 19 MB comes from)
(大胆强调我的)。
这里的错误是假设 Python 进程大小中的所有内存变化都可以直接归因于特定对象,但实际情况要复杂得多,因为内存管理器可以添加 (并删除!) 内存 'arenas',根据需要为堆保留的内存块,如果有意义的话,将在更大的块中这样做。这里的过程比较复杂,因为它依赖于interactions between Python's manager and the OS malloc
implementation details. The author has found an older article on Python's model that they have misunderstood to be current, the 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 团队就 post 展开对话,这篇文章是错误的,令人困惑,而且 Python 2.7 很快就会完全过时,所以他们真的应该关注关于 Python 3 的内存模型。 (*)
当您从 range()
创建一个 新列表 而不是副本时,您会看到与创建 x
相似的内存增加第一次,因为除了新的列表对象之外,您还将创建一组新的整数对象。除了 a specific set of small integers,Python 不会为 range()
操作缓存和重用整数值。
(*) 附录:我打开了issue #6619 with the Thano project. The project agreed with my assessment and removed the page from their documentation,虽然他们还没有更新发布的版本。
我正在尝试复制内存使用测试 here。
本质上,post 声明给出了以下代码片段:
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
此 post 可能已过时 --- 探查器包或 python 可能已更改。无论如何,我的问题是 Python 3.6.x
(1) copy.deepcopy(x)
(如上面代码中定义的)是否应该消耗大量内存?
(2) 为什么我无法复制?
(3) 如果我在 del x
之后重复 x = list(range(1000000))
,内存会增加与我第一次分配 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 万个引用的新列表对象,这个对象在我的 Python 3.6 build on Mac [=126= 上占用了 8MB 多一点的内存] X 10.13(64 位OS):
>>> 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 列表对象过度分配 space 增长,将 range()
对象转换为列表会导致它比使用时多 space 额外增长deepcopy
,因此 x
仍然稍大一些,在必须再次调整大小之前有空间容纳额外的 125k 个对象:
>>> sys.getsizeof(x)
9000112
>>> sys.getsizeof(x) / 2 ** 20
8.583175659179688
>>> ((sys.getsizeof(x) - 64) // 8) - 10**6
125006
而副本只有额外的 space 剩余约 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 and a 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
copies both lists, which allocates again ~50 MB (I am not sure where the additional overhead of 50 MB - 31 MB = 19 MB comes from)
(大胆强调我的)。
这里的错误是假设 Python 进程大小中的所有内存变化都可以直接归因于特定对象,但实际情况要复杂得多,因为内存管理器可以添加 (并删除!) 内存 'arenas',根据需要为堆保留的内存块,如果有意义的话,将在更大的块中这样做。这里的过程比较复杂,因为它依赖于interactions between Python's manager and the OS malloc
implementation details. The author has found an older article on Python's model that they have misunderstood to be current, the 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 团队就 post 展开对话,这篇文章是错误的,令人困惑,而且 Python 2.7 很快就会完全过时,所以他们真的应该关注关于 Python 3 的内存模型。 (*)
当您从 range()
创建一个 新列表 而不是副本时,您会看到与创建 x
相似的内存增加第一次,因为除了新的列表对象之外,您还将创建一组新的整数对象。除了 a specific set of small integers,Python 不会为 range()
操作缓存和重用整数值。
(*) 附录:我打开了issue #6619 with the Thano project. The project agreed with my assessment and removed the page from their documentation,虽然他们还没有更新发布的版本。