NumPy 中的内存管理#

numpy.ndarray 是一个 python 类.它需要额外的内存分配来保存 numpy.ndarray.strides , numpy.ndarray.shapenumpy.ndarray.data 属性.这些属性是在 __new__ 中创建 python 对象后专门分配的. stridesshape 存储在内部分配的一块内存中.

用于存储实际数组值的 data 分配(在 object 数组的情况下可能是指针)可能非常大,因此 NumPy 提供了管理其分配和释放的接口.本文档详细介绍了这些接口的工作方式.

历史概述#

自 1.7.0 版本以来,NumPy 暴露了一组 PyDataMem_* 函数( PyDataMem_NEW , PyDataMem_FREE , PyDataMem_RENEW ),这些函数分别由 alloc , free , realloc 支持.

自从早期以来,Python 也改进了其内存管理能力,并从 3.4 版本开始提供了各种 management policies .这些例程被划分为一组域,每个域都有一个 PyMemAllocatorEx 结构,用于内存管理.Python 还添加了一个 tracemalloc 模块来跟踪对各种例程的调用.这些跟踪钩子已添加到 NumPy 的 PyDataMem_* 例程中.

NumPy 在其内部 npy_alloc_cache , npy_alloc_cache_zeronpy_free_cache 函数中添加了一个已分配内存的小缓存.这些函数分别包装了 alloc , alloc-and-memset(0)free ,但当调用 npy_free_cache 时,它会将指针添加到按大小标记的可用块的简短列表中.这些块可以被随后对 npy_alloc* 的调用重新使用,从而避免内存抖动.

NumPy 中可配置的内存例程 (NEP 49)#

用户可能希望使用自己的例程覆盖内部数据内存例程.由于 NumPy 不使用 Python 域策略来管理数据内存,因此它提供了一组替代的 C-API 来更改内存例程.没有针对大型对象数据块的 Python 域范围策略,因此这些策略不太适合 NumPy 的需求.希望更改 NumPy 数据内存管理例程的用户可以使用 PyDataMem_SetHandler ,它使用 PyDataMem_Handler 结构来保存指向用于管理数据内存的函数的指针.这些调用仍然由内部例程包装,以调用 PyTraceMalloc_Track , PyTraceMalloc_Untrack .由于函数可能会在进程的生命周期内发生更改,因此每个 ndarray 都带有实例化时使用的函数,并且这些函数将用于重新分配或释放实例的数据内存.

type PyDataMem_Handler#

一个用于保存用于操作内存的函数指针的结构体

typedef struct {
    char name[127];  /* multiple of 64 to keep the struct aligned */
    uint8_t version; /* currently 1 */
    PyDataMemAllocator allocator;
} PyDataMem_Handler;

其中分配器结构是

/* The declaration of free differs from PyMemAllocatorEx */
typedef struct {
    void *ctx;
    void* (*malloc) (void *ctx, size_t size);
    void* (*calloc) (void *ctx, size_t nelem, size_t elsize);
    void* (*realloc) (void *ctx, void *ptr, size_t new_size);
    void (*free) (void *ctx, void *ptr, size_t size);
} PyDataMemAllocator;
PyObject *PyDataMem_SetHandler(PyObject *handler)#

设置新的分配策略.如果输入值为 NULL , 将重置策略为默认值.返回之前的策略,如果发生错误则返回 NULL . 我们包装用户提供的函数,以便它们仍然调用 python 和 numpy 内存管理回调钩子.

PyObject *PyDataMem_GetHandler()#

返回将用于为下一个 PyArrayObject 分配数据的当前策略. 如果失败,则返回 NULL .

有关设置和使用 PyDataMem_Handler 的示例,请参见 numpy/_core/tests/test_mem_policy.py 中的测试

如果没有设置策略,则在释放时会发生什么#

一种罕见但有用的技术是在 NumPy 之外分配缓冲区,使用 PyArray_NewFromDescr 将缓冲区包装在 ndarray 中,然后将 OWNDATA 标志切换为 true. 当 ndarray 被释放时,应该调用 ndarrayPyDataMem_Handler 中的相应函数来释放缓冲区. 但是 PyDataMem_Handler 字段从未设置,它将为 NULL . 为了向后兼容,NumPy 将调用 free() 来释放缓冲区. 如果 NUMPY_WARN_IF_NO_MEM_POLICY 设置为 1 ,则会发出警告. 当前默认设置为不发出警告,这可能会在 NumPy 的未来版本中更改.

一种更好的技术是使用 PyCapsule 作为基本对象:

/* define a PyCapsule_Destructor, using the correct deallocator for buff */
void free_wrap(void *capsule){
    void * obj = PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule));
    free(obj);
};

/* then inside the function that creates arr from buff */
...
arr = PyArray_NewFromDescr(... buf, ...);
if (arr == NULL) {
    return NULL;
}
capsule = PyCapsule_New(buf, "my_wrapped_buffer",
                        (PyCapsule_Destructor)&free_wrap);
if (PyArray_SetBaseObject(arr, capsule) == -1) {
    Py_DECREF(arr);
    return NULL;
}
...

使用 np.lib.tracemalloc_domain 进行内存跟踪的示例#

内置的 tracemalloc 模块可用于跟踪 NumPy 内部的分配. NumPy 将其 CPU 内存分配放在 np.lib.tracemalloc_domain 域中. 有关更多信息,请查看: https://docs.python.org/3/library/tracemalloc.html.

这是一个关于如何使用 np.lib.tracemalloc_domain 的例子:

"""
   The goal of this example is to show how to trace memory
   from an application that has NumPy and non-NumPy sections.
   We only select the sections using NumPy related calls.
"""

import tracemalloc
import numpy as np

# Flag to determine if we select NumPy domain
use_np_domain = True

nx = 300
ny = 500

# Start to trace memory
tracemalloc.start()

# Section 1
# ---------

# NumPy related call
a = np.zeros((nx,ny))

# non-NumPy related call
b = [i**2 for i in range(nx*ny)]

snapshot1 = tracemalloc.take_snapshot()
# We filter the snapshot to only select NumPy related calls
np_domain = np.lib.tracemalloc_domain
dom_filter = tracemalloc.DomainFilter(inclusive=use_np_domain,
                                      domain=np_domain)
snapshot1 = snapshot1.filter_traces([dom_filter])
top_stats1 = snapshot1.statistics('traceback')

print("================ SNAPSHOT 1 =================")
for stat in top_stats1:
    print(f"{stat.count} memory blocks: {stat.size / 1024:.1f} KiB")
    print(stat.traceback.format()[-1])

# Clear traces of memory blocks allocated by Python
# before moving to the next section.
tracemalloc.clear_traces()

# Section 2
#----------

# We are only using NumPy
c = np.sum(a*a)

snapshot2 = tracemalloc.take_snapshot()
top_stats2 = snapshot2.statistics('traceback')

print()
print("================ SNAPSHOT 2 =================")
for stat in top_stats2:
    print(f"{stat.count} memory blocks: {stat.size / 1024:.1f} KiB")
    print(stat.traceback.format()[-1])

tracemalloc.stop()

print()
print("============================================")
print("\nTracing Status : ", tracemalloc.is_tracing())

try:
    print("\nTrying to Take Snapshot After Tracing is Stopped.")
    snap = tracemalloc.take_snapshot()
except Exception as e:
    print("Exception : ", e)