NumPy 中的内存管理#

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

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

历史概述#

自版本 1.7.0 以来,NumPy 公开了一组PyDataMem_*函数(PyDataMem_NEWPyDataMem_FREE、 ),它们分别由allocfreereallocPyDataMem_RENEW支持。在该版本中,NumPy 还公开了如下所述的PyDataMem_EventHook函数(现已弃用),该函数包装了操作系统级别的调用。

从早期开始,Python 也改进了其内存管理功能,并从 3.4 版本开始提供各种管理策略。这些例程被分为一组域,每个域都有一个 PyMemAllocatorEx用于内存管理的例程结构。 Python 还添加了一个tracemalloc模块来跟踪对各种例程的调用。这些跟踪挂钩已添加到 NumPyPyDataMem_*例程中。

npy_alloc_cacheNumPy 在其内部、npy_alloc_cache_zero和函数中添加了一个分配内存的小型缓存 npy_free_cache 。这些分别是wrapper allocalloc-and-memset(0)free ,但是当npy_free_cache被调用时,它将指针添加到由大小标记的可用块的短列表中。这些块可以通过后续调用重新使用npy_alloc*,从而避免内存抖动。

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

用户可能希望用自己的程序覆盖内部数据存储器例程。由于 NumPy 不使用 Python 域策略来管理数据内存,因此它提供了一组替代的 C-API 来更改内存例程。对于大块对象数据,没有 Python 域范围的策略,因此这些策略不太适合 NumPy 的需求。希望更改 NumPy 数据内存管理例程的用户可以使用PyDataMem_SetHandler,它使用一个 PyDataMem_Handler结构来保存指向用于管理数据内存的函数的指针。这些调用仍由内部例程包装以调用PyTraceMalloc_TrackPyTraceMalloc_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 内存管理回调挂钩。

PyObject * PyDataMem_GetHandler ()#

返回将用于为下一个分配数据的当前策略PyArrayObject。失败时返回NULL

有关设置和使用 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 的未来版本中发生变化。ndarrayPyDataMem_HandlerPyDataMem_HandlerNULLfree()NUMPY_WARN_IF_NO_MEM_POLICY1

更好的技术是使用 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)