NumPy 中的内存管理#
numpy.ndarray
是一个 python 类.它需要额外的内存分配来保存 numpy.ndarray.strides
、numpy.ndarray.shape
和 numpy.ndarray.data
属性.这些属性在 __new__
中创建 python 对象后专门分配.``strides`` 和 shape
存储在内部分配的一块内存中.
data
分配用于存储实际数组值(在 object
数组的情况下可能是指针)可以非常大,因此 NumPy 提供了管理其分配和释放的接口.本文档详细介绍了这些接口的工作原理.
历史概述#
自版本1.7.0起,NumPy 暴露了一组 PyDataMem_*
函数(PyDataMem_NEW
、PyDataMem_FREE
、PyDataMem_RENEW
),这些函数分别由 alloc、free、realloc 支持.
自早期以来,Python 也提升了其内存管理能力,并从版本 3.4 开始提供了各种 管理策略 .这些例程被分为一组域,每个域都有一个 PyMemAllocatorEx
结构的例程用于内存管理.Python 还添加了一个 tracemalloc
模块来跟踪对各种例程的调用.这些跟踪钩子被添加到 NumPy 的 PyDataMem_*
例程中.
NumPy 在其内部的 npy_alloc_cache
、npy_alloc_cache_zero
和 npy_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 内存管理回调钩子.
有关设置和使用 PyDataMem_Handler 的示例,请参见 numpy/_core/tests/test_mem_policy.py
中的测试.
如果没有设置策略,在释放时会发生什么#
一种罕见但有用的技术是在 NumPy 外部分配一个缓冲区,使用 PyArray_NewFromDescr
将缓冲区包装在一个 ndarray
中,然后将 OWNDATA
标志切换为 true.当 ndarray
被释放时,应调用 ndarray
的 PyDataMem_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
进行内存追踪的示例#
请注意,由于 Python 3.6(或更新版本),内置的 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)