奇怪的 Python 内存分配

Strange Python memory allocation

在弄清楚 Python 的垃圾收集系统如何工作时,我偶然发现了这个奇怪的地方。 运行 这个简单的代码:

import numpy as np
from memory_profiler import profile

@profile
def my_func():
    a = np.random.rand(1000000)
    a = np.append(a, [1])
    a = np.append(a, [2])
    a = np.append(a, [3])
    a = np.append(a, [4])
    a = np.append(a, [5])
    b = np.append(a, [6])
    c = np.append(a, [7])
    d = np.append(a, a)

    return a

if __name__ == '__main__':
    my_func()

在我的 MacBook 上使用 memory_profiler 版本 0.52 和 Python 3.7.6,我得到以下输出:

Line #    Mem usage    Increment   Line Contents
================================================
     4     54.2 MiB     54.2 MiB   @profile
     5                             def my_func():
     6     61.8 MiB      7.7 MiB       a = np.random.rand(1000000)
     7     69.4 MiB      7.6 MiB       a = np.append(a, [1])
     8     69.4 MiB      0.0 MiB       a = np.append(a, [2])
     9     69.4 MiB      0.0 MiB       a = np.append(a, [3])
    10     69.4 MiB      0.0 MiB       a = np.append(a, [4])
    11     69.4 MiB      0.0 MiB       a = np.append(a, [5])
    12     69.4 MiB      0.0 MiB       b = np.append(a, [6])
    13     77.1 MiB      7.6 MiB       c = np.append(a, [7])
    14     92.3 MiB     15.3 MiB       d = np.append(a, a)
    15                             
    16     92.3 MiB      0.0 MiB       return a

有两点很奇怪。首先,为什么第 7 行的内存增加比第 8-11 行更明显?其次,为什么第 12 行没有提供与第 13 行相同的内存增长?

请注意,如果我删除第 12-14 行,我仍然会在第 7 行获得内存增加。所以这不是第 12 行内存实际增加但 memory_profiler 显示不正确的错误在第 7 行增加。

在第 7 行中,这可能是探查器的某种形式的开销吗?从我使用 sys.getsizeof() 可以看出,数组在每次附加时增加 8 个字节,没有突然跳跃。

起初我认为这可能与 Python 列表的情况类似,在这种情况下,每 4 个追加到 32 字节块中就会分配一次内存,但事实似乎并非如此。

没有函数或分析器,我看不到与您在 post 中显示的类似的行为。我能看到的唯一奇怪之处是 d 并不是 a.

大小的两倍
import numpy as np
import sys

a = np.random.rand(1000000)

sys.getsizeof(a)
Out[55]: 8000096

a = np.append(a, [1])

sys.getsizeof(a)
Out[57]: 8000104

a = np.append(a, [2])

sys.getsizeof(a)
Out[59]: 8000112

a = np.append(a, [3])

sys.getsizeof(a)
Out[61]: 8000120

a = np.append(a, [4])

sys.getsizeof(a)
Out[63]: 8000128

a = np.append(a, [5])

sys.getsizeof(a)
Out[66]: 8000136

a = np.append(a, [6])

sys.getsizeof(a)
Out[68]: 8000144

a = np.append(a, [7])

sys.getsizeof(a)
Out[71]: 8000152

d = np.append(a, a)

sys.getsizeof(d)
Out[73]: 16000208

创建 a 生成一个 8e6 字节的数组(检查 `a.nybtes)

 6     61.8 MiB      7.7 MiB       a = np.random.rand(1000000)

np.append 创建一个新数组(它是 concatenate,不是列表附加),所以我们又增加了 8MB。

 7     69.4 MiB      7.6 MiB       a = np.append(a, [1])

我的猜测是,在接下来的步骤中,它会使用这两个 8MB 块来回循环。 numpy 不会 return(到 OS)每个空闲块。

然后将新数组分配给cab 仍然存在。 (我第一次看的时候错过了 b。)

13     77.1 MiB      7.6 MiB       c = np.append(a, [7])

da 大小的两倍,因此占 15MB 的跳跃。 a,b,c 仍然存在。

14     92.3 MiB     15.3 MiB       d = np.append(a, a)

bc 只是比 a 大一两个数字 - 所以每个占用大约 8MB。这似乎说明了一切!

跟踪内存使用情况时,请记住 numpypythonOS 都起作用。我们大多数人都不知道所有的细节,所以我们只能对正在发生的事情做出粗略的猜测。