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
、 ),它们分别由alloc、free、reallocPyDataMem_RENEW
支持。在该版本中,NumPy 还公开了如下所述的PyDataMem_EventHook函数(现已弃用),该函数包装了操作系统级别的调用。
从早期开始,Python 也改进了其内存管理功能,并从 3.4 版本开始提供各种管理策略。这些例程被分为一组域,每个域都有一个
PyMemAllocatorEx
用于内存管理的例程结构。 Python 还添加了一个tracemalloc
模块来跟踪对各种例程的调用。这些跟踪挂钩已添加到 NumPyPyDataMem_*
例程中。
npy_alloc_cache
NumPy 在其内部、npy_alloc_cache_zero
和函数中添加了一个分配内存的小型缓存
npy_free_cache
。这些分别是wrapper 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
、 ,并将使用已弃用的PyDataMem_EventHookFunc
机制。由于函数在进程的生命周期中可能会发生变化,因此每个ndarray
函数都带有在实例化时使用的函数,并且这些函数将用于重新分配或释放实例的数据内存。
-
类型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 *处理程序)#
制定新的分配政策。如果输入值为
NULL
,会将策略重置为默认值。返回之前的策略,或者NULL
如果发生错误则返回。我们包装用户提供的函数,以便它们仍然会调用 python 和 numpy 内存管理回调挂钩。
有关设置和使用 PyDataMem_Handler 的示例,请参阅中的测试
numpy/core/tests/test_mem_policy.py
-
void PyDataMem_EventHookFunc ( void * inp , void * outp , size_t大小, void * user_data ) ; #
该函数将在数据内存操作期间被调用
-
PyDataMem_EventHookFunc * PyDataMem_SetEventHook ( PyDataMem_EventHookFunc * newhook , void * user_data , void * * old_data )#
设置 numpy 数组数据的分配事件挂钩。
返回指向前一个钩子或 的指针
NULL
。如果 old_data 为非 -NULL
,则先前的 user_data 指针将被复制到其中。如果没有
NULL
,钩子将在每个结束时被调用PyDataMem_NEW/FREE/RENEW
:result = PyDataMem_NEW(size) -> (*hook)(NULL, result, size, user_data) PyDataMem_FREE(ptr) -> (*hook)(ptr, NULL, 0, user_data) result = PyDataMem_RENEW(ptr, size) -> (*hook)(ptr, result, size, user_data)
当调用钩子时,GIL 将由调用线程持有。如果钩子执行的操作可能会导致新的分配事件(例如创建/销毁 numpy 对象,或创建/销毁可能导致垃圾回收的 Python 对象),则该钩子应该编写为可重入的。
v1.23 中已弃用
如果没有设置策略,则释放时会发生什么#
一种罕见但有用的技术是在 NumPy 外部分配一个缓冲区,用于
PyArray_NewFromDescr
将缓冲区包装在 a 中ndarray
,然后将OWNDATA
标志切换为 true。当被释放时,应该调用 的ndarray
相应函数来释放缓冲区。但这个领域从来没有被设定过,但它将会是。为了向后兼容,NumPy 将调用释放缓冲区。如果设置为,将发出警告。当前默认设置是不发出警告,这可能会在 NumPy 的未来版本中发生变化。ndarray
PyDataMem_Handler
PyDataMem_Handler
NULL
free()
NUMPY_WARN_IF_NO_MEM_POLICY
1
更好的技术是使用 aPyCapsule
作为基础对象:
/* 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)