Управление памятью в NumPy#

The numpy.ndarray является классом Python. Он требует дополнительных выделений памяти для хранения numpy.ndarray.strides, numpy.ndarray.shape и numpy.ndarray.data атрибуты. Эти атрибуты специально выделяются после создания python-объекта в __new__. strides и shape хранятся в части памяти, выделенной внутренне.

The data выделение памяти, используемое для хранения фактических значений массива (которые могут быть указателями в случае object массивы) могут быть очень большими, поэтому NumPy предоставляет интерфейсы для управления их выделением и освобождением. Этот документ подробно описывает, как работают эти интерфейсы.

Исторический обзор#

Начиная с версии 1.7.0, NumPy предоставляет набор PyDataMem_* функции (PyDataMem_NEW, PyDataMem_FREE, PyDataMem_RENEW), которые поддерживаются определено в, 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 несет с собой функции, использованные во время его создания, и они будут использоваться для перераспределения или освобождения памяти данных экземпляра.

тип 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

PyArray_Descr.metadata (член C)#

Редкий, но полезный прием — выделить буфер вне NumPy, использовать PyArray_NewFromDescr для обертывания буфера в ndarray, затем переключите OWNDATA флаг в true. Когда ndarray выпущен, соответствующая функция из ndarray’s 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#

Встроенный tracemalloc модуль может использоваться для отслеживания выделений памяти внутри NumPy. NumPy помещает свои выделения памяти ЦП в 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)