From 55f2f6cd5ec64f3d4d8cc7e2f940508afa6eb91f Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 11 Oct 2020 14:41:41 +0300 Subject: [PATCH 01/76] ENH: add and use global configurable memory routines --- numpy/core/code_generators/cversions.txt | 3 + numpy/core/code_generators/numpy_api.py | 3 + numpy/core/include/numpy/ndarraytypes.h | 35 +++- numpy/core/setup_common.py | 4 +- numpy/core/src/multiarray/alloc.c | 161 ++++++++++++++++++- numpy/core/src/multiarray/alloc.h | 13 +- numpy/core/src/multiarray/arrayobject.c | 3 +- numpy/core/src/multiarray/arraytypes.c.src | 15 +- numpy/core/src/multiarray/convert_datatype.c | 2 +- numpy/core/src/multiarray/ctors.c | 26 ++- numpy/core/src/multiarray/item_selection.c | 18 ++- numpy/core/src/multiarray/scalartypes.c.src | 14 +- numpy/core/src/multiarray/shape.c | 5 +- 13 files changed, 257 insertions(+), 45 deletions(-) diff --git a/numpy/core/code_generators/cversions.txt b/numpy/core/code_generators/cversions.txt index a02c7153a5c6..4a2e68525612 100644 --- a/numpy/core/code_generators/cversions.txt +++ b/numpy/core/code_generators/cversions.txt @@ -58,3 +58,6 @@ # Version 14 (NumPy 1.21) No change. # Version 14 (NumPy 1.22) No change. 0x0000000e = 17a0f366e55ec05e5c5c149123478452 + +# Version 15 (NumPy 1.20) Configurable memory allocations +0x0000000f = 4177738910303368a00be8b1ce9283d5 diff --git a/numpy/core/code_generators/numpy_api.py b/numpy/core/code_generators/numpy_api.py index fbd3233680fa..a915bbb35b5f 100644 --- a/numpy/core/code_generators/numpy_api.py +++ b/numpy/core/code_generators/numpy_api.py @@ -350,6 +350,9 @@ 'PyArray_ResolveWritebackIfCopy': (302,), 'PyArray_SetWritebackIfCopyBase': (303,), # End 1.14 API + 'PyDataMem_SetHandler': (304,), + 'PyDataMem_GetHandlerName': (305,), + # End 1.20 API } ufunc_types_api = { diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index 84441e641d7c..31b09c371c78 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -355,12 +355,10 @@ struct NpyAuxData_tag { #define NPY_ERR(str) fprintf(stderr, #str); fflush(stderr); #define NPY_ERR2(str) fprintf(stderr, str); fflush(stderr); - /* - * Macros to define how array, and dimension/strides data is - * allocated. - */ - - /* Data buffer - PyDataMem_NEW/FREE/RENEW are in multiarraymodule.c */ +/* +* Macros to define how array, and dimension/strides data is +* allocated. These should be made private +*/ #define NPY_USE_PYMEM 1 @@ -666,6 +664,27 @@ typedef struct _arr_descr { PyObject *shape; /* a tuple */ } PyArray_ArrayDescr; +/* + * Memory handler structure for array data. + */ +typedef void *(PyDataMem_AllocFunc)(size_t size); +typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); +typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); +typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); +typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); + +typedef struct { + char name[128]; /* multiple of 64 to keep the struct unaligned */ + PyDataMem_AllocFunc *alloc; + PyDataMem_ZeroedAllocFunc *zeroed_alloc; + PyDataMem_FreeFunc *free; + PyDataMem_ReallocFunc *realloc; + PyDataMem_CopyFunc *host2obj; /* copy from the host python */ + PyDataMem_CopyFunc *obj2host; /* copy to the host python */ + PyDataMem_CopyFunc *obj2obj; /* copy between two objects */ +} PyDataMem_Handler; + + /* * The main array object structure. * @@ -716,6 +735,10 @@ typedef struct tagPyArrayObject_fields { /* For weak references */ PyObject *weakreflist; void *_buffer_info; /* private buffer info, tagged to allow warning */ + /* + * For alloc/malloc/realloc/free/memcpy per object + */ + PyDataMem_Handler *mem_handler; } PyArrayObject_fields; /* diff --git a/numpy/core/setup_common.py b/numpy/core/setup_common.py index 85c8f16d1e73..70e8fc89705a 100644 --- a/numpy/core/setup_common.py +++ b/numpy/core/setup_common.py @@ -43,8 +43,8 @@ # 0x0000000d - 1.19.x # 0x0000000e - 1.20.x # 0x0000000e - 1.21.x -# 0x0000000e - 1.22.x -C_API_VERSION = 0x0000000e +# 0x0000000f - 1.22.x +C_API_VERSION = 0x0000000f class MismatchCAPIWarning(Warning): pass diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 887deff53457..295c248565d7 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -140,9 +140,10 @@ npy_alloc_cache(npy_uintp sz) /* zero initialized data, sz is number of bytes to allocate */ NPY_NO_EXPORT void * -npy_alloc_cache_zero(npy_uintp sz) +npy_alloc_cache_zero(size_t nmemb, size_t size) { void * p; + size_t sz = nmemb * size; NPY_BEGIN_THREADS_DEF; if (sz < NBUCKETS) { p = _npy_alloc_cache(sz, 1, NBUCKETS, datacache, &PyDataMem_NEW); @@ -152,7 +153,7 @@ npy_alloc_cache_zero(npy_uintp sz) return p; } NPY_BEGIN_THREADS; - p = PyDataMem_NEW_ZEROED(sz, 1); + p = PyDataMem_NEW_ZEROED(nmemb, size); NPY_END_THREADS; return p; } @@ -261,21 +262,21 @@ PyDataMem_NEW(size_t size) * Allocates zeroed memory for array data. */ NPY_NO_EXPORT void * -PyDataMem_NEW_ZEROED(size_t size, size_t elsize) +PyDataMem_NEW_ZEROED(size_t nmemb, size_t size) { void *result; - result = calloc(size, elsize); + result = calloc(nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API if (_PyDataMem_eventhook != NULL) { - (*_PyDataMem_eventhook)(NULL, result, size * elsize, + (*_PyDataMem_eventhook)(NULL, result, nmemb * size, _PyDataMem_eventhook_user_data); } NPY_DISABLE_C_API } - PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, size); + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, nmemb * size); return result; } @@ -323,3 +324,151 @@ PyDataMem_RENEW(void *ptr, size_t size) } return result; } + +typedef void *(alloc_wrapper)(size_t, PyDataMem_AllocFunc *); +typedef void *(zalloc_wrapper)(size_t nelems, size_t elsize); +typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); +typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); +typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); + +/* Memory handler global default */ +static PyDataMem_Handler default_allocator = { + "default_allocator", + npy_alloc_cache, /* alloc */ + npy_alloc_cache_zero, /* zeroed_alloc */ + npy_free_cache, /* free */ + PyDataMem_RENEW, /* realloc */ + memcpy, /* host2obj */ + memcpy, /* obj2host */ + memcpy, /* obj2obj */ +}; + +PyDataMem_Handler *current_allocator = &default_allocator; + +int uo_index=0; /* user_override index */ + +/* Wrappers for user-assigned PyDataMem_Handlers */ + +NPY_NO_EXPORT void * +PyDataMem_UserNEW(size_t size, PyDataMem_AllocFunc *alloc) +{ + void *result; + + if (alloc == npy_alloc_cache) { + // All the logic below is conditionally handled by npy_alloc_cache + return npy_alloc_cache(size); + } + assert(size != 0); + result = alloc(size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(NULL, result, size, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, size); + return result; +} + +NPY_NO_EXPORT void * +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *zalloc) +{ + void *result; + if (zalloc == npy_alloc_cache_zero) { + // All the logic below is conditionally handled by npy_alloc_cache_zero + return npy_alloc_cache_zero(nmemb, size); + } + + result = zalloc(nmemb, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(NULL, result, nmemb * size, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, nmemb * size); + return result; +} + +NPY_NO_EXPORT void +PyDataMem_UserFREE(void *ptr, size_t size, PyDataMem_FreeFunc *func) +{ + if (func == npy_free_cache) { + // All the logic below is conditionally handled by npy_free_cache + return npy_free_cache(ptr, size); + } + PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); + func(ptr, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(ptr, NULL, 0, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } +} + +NPY_NO_EXPORT void * +PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func) +{ + void *result; + + assert(size != 0); + result = func(ptr, size); + if (result != ptr) { + PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); + } + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(ptr, result, size, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } + return result; +} + +/*NUMPY_API + * Sets a new allocation policy. If the input value is NULL, will reset + * the policy to the default. Returns the previous policy, NULL if the + * previous policy was the default. We wrap the user-provided functions + * so they will still call the python and numpy memory management callback + * hooks. + */ +NPY_NO_EXPORT const PyDataMem_Handler * +PyDataMem_SetHandler(PyDataMem_Handler *handler) +{ + const PyDataMem_Handler *old = current_allocator; + if (handler) { + current_allocator = handler; + } + else { + current_allocator = &default_allocator; + } + return old; +} + +/*NUMPY_API + * Return the const char name of the PyDataMem_Handler used by the + * PyArrayObject. If NULL, return the name of the current global policy that + * will be used to allocate data for the next PyArrayObject + */ +NPY_NO_EXPORT const char * +PyDataMem_GetHandlerName(PyArrayObject *obj) +{ + if (obj == NULL) { + return current_allocator->name; + } + return PyArray_HANDLER(obj)->name; +} diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index 15e31ebb5f2f..5a1726090358 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -10,13 +10,16 @@ NPY_NO_EXPORT PyObject * _set_madvise_hugepage(PyObject *NPY_UNUSED(self), PyObject *enabled_obj); NPY_NO_EXPORT void * -npy_alloc_cache(npy_uintp sz); +PyDataMem_UserNEW(npy_uintp sz, PyDataMem_AllocFunc *alloc); NPY_NO_EXPORT void * -npy_alloc_cache_zero(npy_uintp sz); +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *zalloc); NPY_NO_EXPORT void -npy_free_cache(void * p, npy_uintp sd); +PyDataMem_UserFREE(void * p, npy_uintp sd, PyDataMem_FreeFunc *func); + +NPY_NO_EXPORT void * +PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func); NPY_NO_EXPORT void * npy_alloc_cache_dim(npy_uintp sz); @@ -36,4 +39,8 @@ npy_free_cache_dim_array(PyArrayObject * arr) npy_free_cache_dim(PyArray_DIMS(arr), PyArray_NDIM(arr)); } +extern PyDataMem_Handler *current_allocator; + +#define PyArray_HANDLER(arr) ((PyArrayObject_fields*)(arr))->mem_handler + #endif diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index 55ba5601b4ed..b439c11374fd 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -493,7 +493,8 @@ array_dealloc(PyArrayObject *self) if (PyDataType_FLAGCHK(fa->descr, NPY_ITEM_REFCOUNT)) { PyArray_XDECREF(self); } - npy_free_cache(fa->data, PyArray_NBYTES(self)); + PyDataMem_UserFREE(fa->data, PyArray_NBYTES(self), + fa->mem_handler->free); } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index b3ea7544d974..22c62c20d319 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3107,15 +3107,17 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (_unpack_field(tup, &new, &offset) < 0) { goto finish; } - /* descr is the only field checked by compare or copyswap */ + /* Set the fields needed by compare or copyswap */ dummy_struct.descr = new; + dummy_struct.mem_handler = PyArray_HANDLER(ap); + swap = PyArray_ISBYTESWAPPED(dummy); nip1 = ip1 + offset; nip2 = ip2 + offset; if (swap || new->alignment > 1) { if (swap || !npy_is_aligned(nip1, new->alignment)) { /* create buffer and copy */ - nip1 = npy_alloc_cache(new->elsize); + nip1 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->alloc); if (nip1 == NULL) { goto finish; } @@ -3125,10 +3127,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } if (swap || !npy_is_aligned(nip2, new->alignment)) { /* create buffer and copy */ - nip2 = npy_alloc_cache(new->elsize); + nip2 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->alloc); if (nip2 == NULL) { if (nip1 != ip1 + offset) { - npy_free_cache(nip1, new->elsize); + PyDataMem_UserFREE(nip1, new->elsize, + PyArray_HANDLER(ap)->free); } goto finish; } @@ -3140,10 +3143,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, dummy); if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { - npy_free_cache(nip1, new->elsize); + PyDataMem_UserFREE(nip1, new->elsize, PyArray_HANDLER(ap)->free); } if (nip2 != ip2 + offset) { - npy_free_cache(nip2, new->elsize); + PyDataMem_UserFREE(nip2, new->elsize, PyArray_HANDLER(ap)->free); } } if (res != 0) { diff --git a/numpy/core/src/multiarray/convert_datatype.c b/numpy/core/src/multiarray/convert_datatype.c index e3b25d0763b6..d89553293934 100644 --- a/numpy/core/src/multiarray/convert_datatype.c +++ b/numpy/core/src/multiarray/convert_datatype.c @@ -2097,7 +2097,7 @@ PyArray_ObjectType(PyObject *op, int minimum_type) * This function is only used in one place within NumPy and should * generally be avoided. It is provided mainly for backward compatibility. * - * The user of the function has to free the returned array. + * The user of the function has to free the returned array with PyDataMem_FREE. */ NPY_NO_EXPORT PyArrayObject ** PyArray_ConvertToCommonType(PyObject *op, int *retn) diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index aaa645c1650d..f4161bfb0689 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -804,6 +804,9 @@ PyArray_NewFromDescr_int( fa->flags |= NPY_ARRAY_C_CONTIGUOUS|NPY_ARRAY_F_CONTIGUOUS; } + /* Store the functions in case the global hander is modified */ + fa->mem_handler = current_allocator; + if (data == NULL) { /* * Allocate something even for zero-space arrays @@ -819,15 +822,17 @@ PyArray_NewFromDescr_int( * which could also be sub-fields of a VOID array */ if (zeroed || PyDataType_FLAGCHK(descr, NPY_NEEDS_INIT)) { - data = npy_alloc_cache_zero(nbytes); + data = PyDataMem_UserNEW_ZEROED(nbytes, 1, + fa->mem_handler->zeroed_alloc); } else { - data = npy_alloc_cache(nbytes); + data = PyDataMem_UserNEW(nbytes, fa->mem_handler->alloc); } if (data == NULL) { raise_memory_error(fa->nd, fa->dimensions, descr); goto fail; } + fa->flags |= NPY_ARRAY_OWNDATA; } else { @@ -3401,7 +3406,8 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre dptr += dtype->elsize; if (num < 0 && thisbuf == size) { totalbytes += bytes; - tmp = PyDataMem_RENEW(PyArray_DATA(r), totalbytes); + tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes, + PyArray_HANDLER(r)->realloc); if (tmp == NULL) { err = 1; break; @@ -3423,7 +3429,8 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre const size_t nsize = PyArray_MAX(*nread,1)*dtype->elsize; if (nsize != 0) { - tmp = PyDataMem_RENEW(PyArray_DATA(r), nsize); + tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize, + PyArray_HANDLER(r)->realloc); if (tmp == NULL) { err = 1; } @@ -3528,7 +3535,8 @@ PyArray_FromFile(FILE *fp, PyArray_Descr *dtype, npy_intp num, char *sep) const size_t nsize = PyArray_MAX(nread,1) * dtype->elsize; char *tmp; - if ((tmp = PyDataMem_RENEW(PyArray_DATA(ret), nsize)) == NULL) { + if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize, + PyArray_HANDLER(ret)->realloc)) == NULL) { Py_DECREF(dtype); Py_DECREF(ret); return PyErr_NoMemory(); @@ -3812,7 +3820,8 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) */ elcount = (i >> 1) + (i < 4 ? 4 : 2) + i; if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) { - new_data = PyDataMem_RENEW(PyArray_DATA(ret), nbytes); + new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes, + PyArray_HANDLER(ret)->realloc); } else { new_data = NULL; @@ -3850,10 +3859,11 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) * (assuming realloc is reasonably good about reusing space...) */ if (i == 0 || elsize == 0) { - /* The size cannot be zero for PyDataMem_RENEW. */ + /* The size cannot be zero for realloc. */ goto done; } - new_data = PyDataMem_RENEW(PyArray_DATA(ret), i * elsize); + new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize, + PyArray_HANDLER(ret)->realloc); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate array memory"); diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 2b8ea9e79ace..1d48916098a9 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -776,6 +776,7 @@ PyArray_Repeat(PyArrayObject *aop, PyObject *op, int axis) return NULL; } + /*NUMPY_API */ NPY_NO_EXPORT PyObject * @@ -907,7 +908,7 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out, Py_XDECREF(mps[i]); } Py_DECREF(ap); - npy_free_cache(mps, n * sizeof(mps[0])); + PyDataMem_FREE(mps); if (out != NULL && out != obj) { Py_INCREF(out); PyArray_ResolveWritebackIfCopy(obj); @@ -922,7 +923,7 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out, Py_XDECREF(mps[i]); } Py_XDECREF(ap); - npy_free_cache(mps, n * sizeof(mps[0])); + PyDataMem_FREE(mps); PyArray_DiscardWritebackIfCopy(obj); Py_XDECREF(obj); return NULL; @@ -969,7 +970,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, size = it->size; if (needcopy) { - buffer = npy_alloc_cache(N * elsize); + buffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->alloc); if (buffer == NULL) { ret = -1; goto fail; @@ -1053,7 +1054,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - npy_free_cache(buffer, N * elsize); + PyDataMem_UserFREE(buffer, N * elsize, PyArray_HANDLER(op)->free); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -1115,7 +1116,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = npy_alloc_cache(N * elsize); + valbuffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->alloc); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1123,7 +1124,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, } if (needidxbuffer) { - idxbuffer = (npy_intp *)npy_alloc_cache(N * sizeof(npy_intp)); + idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), + PyArray_HANDLER(op)->alloc); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1212,8 +1214,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - npy_free_cache(valbuffer, N * elsize); - npy_free_cache(idxbuffer, N * sizeof(npy_intp)); + PyDataMem_UserFREE(valbuffer, N * elsize, PyArray_HANDLER(op)->free); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), PyArray_HANDLER(op)->free); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index 40f736125de3..2d581845eb98 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -34,6 +34,16 @@ #include "binop_override.h" +/* + * used for allocating a single scalar, so use the default numpy + * memory allocators instead of the (maybe) user overrides + */ +NPY_NO_EXPORT void * +npy_alloc_cache_zero(size_t nmemb, size_t size); + +NPY_NO_EXPORT void +npy_free_cache(void * p, npy_uintp sz); + NPY_NO_EXPORT PyBoolScalarObject _PyArrayScalar_BoolValues[] = { {PyObject_HEAD_INIT(&PyBoolArrType_Type) 0}, {PyObject_HEAD_INIT(&PyBoolArrType_Type) 1}, @@ -1321,7 +1331,7 @@ gentype_imag_get(PyObject *self, void *NPY_UNUSED(ignored)) int elsize; typecode = PyArray_DescrFromScalar(self); elsize = typecode->elsize; - temp = npy_alloc_cache_zero(elsize); + temp = npy_alloc_cache_zero(1, elsize); ret = PyArray_Scalar(temp, typecode, NULL); npy_free_cache(temp, elsize); } @@ -3022,7 +3032,7 @@ void_arrtype_new(PyTypeObject *type, PyObject *args, PyObject *kwds) (int) NPY_MAX_INT); return NULL; } - destptr = npy_alloc_cache_zero(memu); + destptr = npy_alloc_cache_zero(memu, 1); if (destptr == NULL) { return PyErr_NoMemory(); } diff --git a/numpy/core/src/multiarray/shape.c b/numpy/core/src/multiarray/shape.c index 02c349759528..a67063987c89 100644 --- a/numpy/core/src/multiarray/shape.c +++ b/numpy/core/src/multiarray/shape.c @@ -120,8 +120,9 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, } /* Reallocate space if needed - allocating 0 is forbidden */ - new_data = PyDataMem_RENEW( - PyArray_DATA(self), newnbytes == 0 ? elsize : newnbytes); + new_data = PyDataMem_UserRENEW(PyArray_DATA(self), + newnbytes == 0 ? elsize : newnbytes, + PyArray_HANDLER(self)->realloc); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory for array"); From 23da73e3d9fd4fbfdb994a7521ca2e6014be3ca2 Mon Sep 17 00:00:00 2001 From: mattip Date: Fri, 16 Oct 2020 16:23:00 +0300 Subject: [PATCH 02/76] ENH: add tests and a way to compile c-extensions from tests --- doc/TESTS.rst.txt | 15 ++ numpy/core/tests/test_mem_policy.py | 143 ++++++++++++++++ numpy/testing/__init__.py | 2 +- numpy/testing/_private/extbuild.py | 248 ++++++++++++++++++++++++++++ 4 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 numpy/core/tests/test_mem_policy.py create mode 100644 numpy/testing/_private/extbuild.py diff --git a/doc/TESTS.rst.txt b/doc/TESTS.rst.txt index ba09aa80028a..0b9cc610eb1e 100644 --- a/doc/TESTS.rst.txt +++ b/doc/TESTS.rst.txt @@ -145,6 +145,21 @@ originally written without unit tests, there are still several modules that don't have tests yet. Please feel free to choose one of these modules and develop tests for it. +Using C code in tests +--------------------- + +NumPy exposes a rich :ref:`C-API` . These are tested using c-extension +modules written "as-if" they know nothing about the internals of NumPy, rather +using the official C-API interfaces only. Examples of such modules are tests +for a user-defined ``rational`` dtype in ``_rational_tests`` or the ufunc +machinery tests in ``_umath_tests`` which are part of the binary distribution. +Starting from version 1.20, you can also write snippets of C code in tests that +will be compiled locally into c-extension modules and loaded into python. + +.. currentmodule:: numpy.testing.extbuild + +.. autofunction:: build_and_import_extension + Labeling tests -------------- diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py new file mode 100644 index 000000000000..f6320f66410c --- /dev/null +++ b/numpy/core/tests/test_mem_policy.py @@ -0,0 +1,143 @@ +import pathlib +import pytest +import tempfile +import numpy as np +from numpy.testing import extbuild + +@pytest.fixture +def get_module(tmp_path): + """ Add a memory policy that returns a false pointer 64 bytes into the + actual allocation, and fill the prefix with some text. Then check at each + memory manipulation that the prefix exists, to make sure all alloc/realloc/ + free/calloc go via the functions here. + """ + functions = [( + "test_prefix", "METH_O", + """ + if (!PyArray_Check(args)) { + PyErr_SetString(PyExc_ValueError, + "must be called with a numpy scalar or ndarray"); + } + return PyUnicode_FromString(PyDataMem_GetHandlerName((PyArrayObject*)args)); + """ + ), + ("set_new_policy", "METH_NOARGS", + """ + const PyDataMem_Handler *old = PyDataMem_SetHandler(&new_handler); + return PyUnicode_FromString(old->name); + """), + ("set_old_policy", "METH_NOARGS", + """ + const PyDataMem_Handler *old = PyDataMem_SetHandler(NULL); + return PyUnicode_FromString(old->name); + """), + ] + prologue=''' + #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION + #include + NPY_NO_EXPORT void * + shift_alloc(size_t sz) { + char *real = (char *)malloc(sz + 64); + if (real == NULL) { + return NULL; + } + snprintf(real, 64, "originally allocated %ld", sz); + return (void *)(real + 64); + } + NPY_NO_EXPORT void * + shift_zero(size_t sz, size_t cnt) { + char *real = (char *)calloc(sz + 64, cnt); + if (real == NULL) { + return NULL; + } + snprintf(real, 64, "originally allocated %ld", sz); + return (void *)(real + 64); + } + NPY_NO_EXPORT void + shift_free(void * p, npy_uintp sz) { + if (p == NULL) { + return ; + } + char *real = (char *)p - 64; + if (strncmp(real, "originally allocated", 20) != 0) { + fprintf(stdout, "uh-oh, unmatched shift_free\\n"); + /* Make gcc crash by calling free on the wrong address */ + free((char *)p + 10); + /* free(p); */ + } + else { + free(real); + } + } + NPY_NO_EXPORT void * + shift_realloc(void * p, npy_uintp sz) { + if (p != NULL) { + char *real = (char *)p - 64; + if (strncmp(real, "originally allocated", 20) != 0) { + fprintf(stdout, "uh-oh, unmatched shift_realloc\\n"); + return realloc(p, sz); + } + return (void *)((char *)realloc(real, sz + 64) + 64); + } + else { + char *real = (char *)realloc(p, sz + 64); + if (real == NULL) { + return NULL; + } + snprintf(real, 64, "originally allocated (realloc) %ld", sz); + return (void *)(real + 64); + } + } + static PyDataMem_Handler new_handler = { + "secret_data_allocator", + shift_alloc, /* alloc */ + shift_zero, /* zeroed_alloc */ + shift_free, /* free */ + shift_realloc, /* realloc */ + memcpy, /* host2obj */ + memcpy, /* obj2host */ + memcpy, /* obj2obj */ + }; + ''' + more_init="import_array();" + try: + import mem_policy + return mem_policy + except ImportError: + pass + # if it does not exist, build and load it + try: + return extbuild.build_and_import_extension('mem_policy', + functions, prologue=prologue, include_dirs=[np.get_include()], + build_dir=tmp_path, more_init=more_init) + except: + raise + pytest.skip("could not build module") + + +def test_set_policy(get_module): + a = np.arange(10) + orig_policy = get_module.test_prefix(a) + assert get_module.set_new_policy() == orig_policy + if orig_policy == 'default_allocator': + get_module.set_old_policy() + +@pytest.mark.slow +def test_new_policy(get_module): + a = np.arange(10) + orig_policy = get_module.test_prefix(a) + assert get_module.set_new_policy() == orig_policy + b = np.arange(10) + assert get_module.test_prefix(b) == 'secret_data_allocator' + + # test array manipulation. This is slow + if orig_policy == 'default_allocator': + # when the test set recurses into this test, the policy will be set + # so this "if" will be false, preventing infinite recursion + # + # if needed, debug this by setting extra_argv=['-vvx'] + np.core.test(verbose=0, extra_argv=[]) + get_module.set_old_policy() + assert get_module.test_prefix(a) == orig_policy + c = np.arange(10) + assert get_module.test_prefix(c) == 'default_allocator' diff --git a/numpy/testing/__init__.py b/numpy/testing/__init__.py index bca1d3670233..a008f5828e58 100644 --- a/numpy/testing/__init__.py +++ b/numpy/testing/__init__.py @@ -10,7 +10,7 @@ from ._private.utils import * from ._private.utils import (_assert_valid_refcount, _gen_alignment_data, IS_PYSTON) -from ._private import decorators as dec +from ._private import extbuild, decorators as dec from ._private.nosetester import ( run_module_suite, NoseTester as Tester ) diff --git a/numpy/testing/_private/extbuild.py b/numpy/testing/_private/extbuild.py new file mode 100644 index 000000000000..a94ca94c1821 --- /dev/null +++ b/numpy/testing/_private/extbuild.py @@ -0,0 +1,248 @@ +""" +Build a c-extension module on-the-fly in tests. +See build_and_import_extensions for usage hints + +""" + +import os +import pathlib +import sys +import setuptools +import sysconfig +from distutils.ccompiler import new_compiler +from distutils.sysconfig import customize_compiler +from distutils.errors import CompileError + +__all__ = ['build_and_import_extensions', 'compile_extension_module'] + +def build_and_import_extension(modname, functions, *, prologue="", build_dir=None, + include_dirs=[], more_init="", + ): + """ + Build and imports a c-extension module `modname` from a list of function + fragments `functions`. + + + Parameters + ---------- + functions : list of fragments + Each fragment is a sequence of func_name, calling convention, snippet. + prologue : string + Code to preceed the rest, usually extra ``#include`` or ``#define`` + macros. + build_dir : pathlib.Path + Where to build the module, usually a temporary directory + include_dirs : list + Extra directories to find include files when compiling + more_init : string + Code to appear in the module PyMODINIT_FUNC + + Returns + ------- + out: module + The module will have been loaded and is ready for use + + Examples + -------- + >>> functions = [("test_bytes", "METH_O", \"\"\" + if ( !PyBytesCheck(args)) { + Py_RETURN_FALSE; + } + Py_RETURN_TRUE; + \"\"\")] + >>> mod = build_and_import_extension("testme", functions) + >>> assert not mod.test_bytes(u'abc') + >>> assert mod.test_bytes(b'abc') + """ + + body = prologue + _make_methods(functions, modname) + init = """PyObject *mod = PyModule_Create(&moduledef); + """ + if not build_dir: + build_dir = pathlib.Path('.') + if more_init: + init += """#define INITERROR return NULL + """ + init += more_init + init += "\nreturn mod;" + source_string = _make_source(modname, init, body) + try: + mod_so = compile_extension_module( + modname, build_dir, include_dirs, source_string) + except CompileError as e: + # shorten the exception chain + raise RuntimeError(f"could not compile in {build_dir}:") from e + import importlib.util + spec = importlib.util.spec_from_file_location(modname, mod_so) + foo = importlib.util.module_from_spec(spec) + spec.loader.exec_module(foo) + return foo + +def compile_extension_module(name, builddir, include_dirs, + source_string, libraries=[], library_dirs=[]): + """ + Build an extension module and return the filename of the resulting + native code file. + + Parameters + ---------- + name : string + name of the module, possibly including dots if it is a module inside a + package. + builddir : pathlib.Path + Where to build the module, usually a temporary directory + include_dirs : list + Extra directories to find include files when compiling + libraries : list + Libraries to link into the extension module + library_dirs: list + Where to find the libraries, ``-L`` passed to the linker + """ + modname = name.split('.')[-1] + dirname = builddir / name + dirname.mkdir(exist_ok=True) + cfile = _convert_str_to_file(source_string, dirname) + include_dirs = [sysconfig.get_config_var('INCLUDEPY')] + include_dirs + + return _c_compile(cfile, outputfilename= dirname / modname, + include_dirs=include_dirs, libraries=[], library_dirs=[], + ) + +def _convert_str_to_file(source, dirname): + """Helper function to create a file ``source.c`` in `dirname` that contains + the string in `source`. Returns the file name + """ + filename = dirname / 'source.c' + with filename.open('w') as f: + f.write(str(source)) + return filename + + +def _make_methods(functions, modname): + """ Turns the name, signature, code in functions into complete functions + and lists them in a methods_table. Then turns the methods_table into a + ``PyMethodDef`` structure and returns the resulting code fragment ready + for compilation + """ + methods_table = [] + codes = [] + for funcname, flags, code in functions: + cfuncname = "%s_%s" % (modname, funcname) + if 'METH_KEYWORDS' in flags: + signature = '(PyObject *self, PyObject *args, PyObject *kwargs)' + else: + signature = '(PyObject *self, PyObject *args)' + methods_table.append( + "{\"%s\", (PyCFunction)%s, %s}," % (funcname, cfuncname, flags)) + func_code = """ + static PyObject* {cfuncname}{signature} + {{ + {code} + }} + """.format(cfuncname=cfuncname, signature=signature, code=code) + codes.append(func_code) + + body = "\n".join(codes) + """ + static PyMethodDef methods[] = { + %(methods)s + { NULL } + }; + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "%(modname)s", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + methods, /* m_methods */ + }; + """ % dict(methods='\n'.join(methods_table), modname=modname) + return body + +def _make_source(name, init, body): + """ Combines the code fragments into source code ready to be compiled + """ + code = """ + #include + + %(body)s + + PyMODINIT_FUNC + PyInit_%(name)s(void) { + %(init)s + } + """ % dict( + name=name, init=init, body=body, + ) + return code + + +def _c_compile(cfile, outputfilename, include_dirs=[], libraries=[], + library_dirs=[]): + if sys.platform == 'win32': + compile_extra = ["/we4013"] + link_extra = ["/LIBPATH:" + os.path.join(sys.exec_prefix, 'libs')] + elif sys.platform.startswith('linux'): + compile_extra = [ + "-O0", "-g", "-Werror=implicit-function-declaration", "-fPIC"] + link_extra = None + else: + compile_extra = link_extra = None + pass + if sys.platform == 'win32': + link_extra = link_extra + ['/DEBUG'] # generate .pdb file + if sys.platform == 'darwin': + # support Fink & Darwinports + for s in ('/sw/', '/opt/local/'): + if (s + 'include' not in include_dirs + and os.path.exists(s + 'include')): + include_dirs.append(s + 'include') + if s + 'lib' not in library_dirs and os.path.exists(s + 'lib'): + library_dirs.append(s + 'lib') + + outputfilename = outputfilename.with_suffix(get_so_suffix()) + saved_environ = os.environ.copy() + try: + build( + cfile, outputfilename, + compile_extra, link_extra, + include_dirs, libraries, library_dirs) + finally: + # workaround for a distutils bugs where some env vars can + # become longer and longer every time it is used + for key, value in saved_environ.items(): + if os.environ.get(key) != value: + os.environ[key] = value + return outputfilename + +def build(cfile, outputfilename, compile_extra, link_extra, + include_dirs, libraries, library_dirs): + "cd into the directory where the cfile is, use distutils to build" + + compiler = new_compiler(force=1) + compiler.verbose = 1 + customize_compiler(compiler) + objects = [] + + old = os.getcwd() + os.chdir(cfile.parent) + try: + res = compiler.compile([str(cfile.name)], + include_dirs=include_dirs, extra_preargs=compile_extra) + objects += [str(cfile.parent / r) for r in res] + finally: + os.chdir(old) + + compiler.link_shared_object( + objects, str(outputfilename), + libraries=libraries, + extra_preargs=link_extra, + library_dirs=library_dirs) + +def get_so_suffix(): + ret = sysconfig.get_config_var('EXT_SUFFIX') + assert ret + return ret + +def get_extbuilder(base_dir): + return ExtensionCompiler( + builddir_base=base_dir, + ) From 94b9f25961d5c2a99c87bbe33adbedae557bde4c Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 18 Oct 2020 19:22:55 +0300 Subject: [PATCH 03/76] fix allocation/free exposed by tests --- numpy/core/src/multiarray/getset.c | 3 ++- numpy/core/src/multiarray/methods.c | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index de2a8c14e6aa..c2ced1b305f2 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -384,7 +384,8 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) } if (PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA) { PyArray_XDECREF(self); - PyDataMem_FREE(PyArray_DATA(self)); + PyDataMem_UserFREE(PyArray_DATA(self), PyArray_NBYTES(self), + PyArray_HANDLER(self)->free); } if (PyArray_BASE(self)) { if ((PyArray_FLAGS(self) & NPY_ARRAY_WRITEBACKIFCOPY) || diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index dc23b3471cb2..008d51411eec 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2039,7 +2039,8 @@ array_setstate(PyArrayObject *self, PyObject *args) } if ((PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA)) { - PyDataMem_FREE(PyArray_DATA(self)); + PyDataMem_UserFREE(PyArray_DATA(self), PyArray_NBYTES(self), + PyArray_HANDLER(self)->free); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } Py_XDECREF(PyArray_BASE(self)); @@ -2084,7 +2085,7 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); Py_RETURN_NONE; } - fa->data = PyDataMem_NEW(num); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->alloc); if (PyArray_DATA(self) == NULL) { Py_DECREF(rawdata); return PyErr_NoMemory(); @@ -2132,7 +2133,7 @@ array_setstate(PyArrayObject *self, PyObject *args) if (num == 0 || elsize == 0) { Py_RETURN_NONE; } - fa->data = PyDataMem_NEW(num); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->alloc); if (PyArray_DATA(self) == NULL) { return PyErr_NoMemory(); } From 81b45fd264eafe29b44179cb34037f5e0a8336c6 Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 18 Oct 2020 22:24:45 +0300 Subject: [PATCH 04/76] DOC: document the new APIs (and some old ones too) --- doc/source/reference/c-api/data_memory.rst | 96 ++++++++++++++++++++++ doc/source/reference/c-api/index.rst | 1 + 2 files changed, 97 insertions(+) create mode 100644 doc/source/reference/c-api/data_memory.rst diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst new file mode 100644 index 000000000000..9023329cbdd0 --- /dev/null +++ b/doc/source/reference/c-api/data_memory.rst @@ -0,0 +1,96 @@ +Memory management +----------------- + +The `numpy.ndarray` is a python class. It requires additinal memory allocations +to hold `numpy.ndarray.strides`, `numpy.ndarray.shape` and +``numpy.ndarray.data`` attributes. These attributes are specially allocated +after creating the python object in `__new__`. The ``strides`` and +``dimensions`` are stored in a piece of memory allocated internally. + +These allocations are small relative to the ``data``, the homogeneous chunk of +memory used to store the actual array values (which could be pointers in the +case of ``object`` arrays). Users may wish to override the internal data +memory routines with ones of their own. They can do this by using +``PyDataMem_SetHandler``, which uses a ``PyDataMem_Handler`` structure to hold +pointers to functions used to manage the data memory. The calls are wrapped +by internal routines to call :c:func:`PyTraceMalloc_Track`, +:c:func:`PyTraceMalloc_Untrack`, and will use the `PyDataMem_EventHookFunc` +mechanism. Since the functions may change during the lifetime of the process, +each `ndarray` carries with it the functions used at the time of its +instantiation, and these will be used to reallocate or free the data memory of +the instance. + +.. c:type:: PyDataMem_Handler + + A struct to hold function pointers used to manipulate memory + + .. code-block:: c + + typedef struct { + char name[128]; /* multiple of 64 to keep the struct unaligned */ + PyDataMem_AllocFunc *alloc; + PyDataMem_ZeroedAllocFunc *zeroed_alloc; + PyDataMem_FreeFunc *free; + PyDataMem_ReallocFunc *realloc; + PyDataMem_CopyFunc *host2obj; /* copy from the host python */ + PyDataMem_CopyFunc *obj2host; /* copy to the host python */ + PyDataMem_CopyFunc *obj2obj; /* copy between two objects */ + } PyDataMem_Handler; + + where the function's signatures are + + .. code-block:: c + + typedef void *(PyDataMem_AllocFunc)(size_t size); + typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); + typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); + typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); + typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); + +.. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) + + Sets a new allocation policy. If the input value is NULL, will reset + the policy to the default. Returns the previous policy, NULL if the + previous policy was the default. We wrap the user-provided functions + so they will still call the python and numpy memory management callback + hooks. + +.. c:function:: const char * PyDataMem_GetHandlerName(PyArrayObject *obj) + + Return the const char name of the PyDataMem_Handler used by the + PyArrayObject. If NULL, return the name of the current global policy that + will be used to allocate data for the next PyArrayObject + +For an example of setting up and using the PyDataMem_Handler, see the test in +``numpy/core/tests/test_mem_policy.py`` + +.. c:function:: typedef void (PyDataMem_EventHookFunc)(void *inp, void *outp, + size_t size, void *user_data); + + This function will be called on NEW,FREE,RENEW calls in data memory + manipulation + + + +.. c:function:: PyDataMem_EventHookFunc * PyDataMem_SetEventHook( + PyDataMem_EventHookFunc *newhook, void *user_data, void **old_data) + + Sets the allocation event hook for numpy array data. + + Returns a pointer to the previous hook or NULL. If old_data is + non-NULL, the previous user_data pointer will be copied to it. + + If not NULL, hook will be called at the end of each PyDataMem_NEW/FREE/RENEW: + + .. code-block:: c + + 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) + + When the hook is called, the GIL will be held by the calling + thread. The hook should be written to be reentrant, if it performs + operations that might cause new allocation events (such as the + creation/destruction numpy objects, or creating/destroying Python + objects which might cause a gc) + diff --git a/doc/source/reference/c-api/index.rst b/doc/source/reference/c-api/index.rst index bb1ed154e9b0..6288ff33bc17 100644 --- a/doc/source/reference/c-api/index.rst +++ b/doc/source/reference/c-api/index.rst @@ -49,3 +49,4 @@ code. generalized-ufuncs coremath deprecations + data_memory From fc32c2fb54d8b2d1f9340d37137350ac89d0db16 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Mon, 19 Oct 2020 16:12:25 +0300 Subject: [PATCH 05/76] BUG: return void from FREE, also some cleanup --- numpy/core/src/multiarray/alloc.c | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 295c248565d7..c123be1529e4 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -195,8 +195,8 @@ npy_free_cache_dim(void * p, npy_uintp sz) /* malloc/free/realloc hook */ -NPY_NO_EXPORT PyDataMem_EventHookFunc *_PyDataMem_eventhook; -NPY_NO_EXPORT void *_PyDataMem_eventhook_user_data; +NPY_NO_EXPORT PyDataMem_EventHookFunc *_PyDataMem_eventhook = NULL; +NPY_NO_EXPORT void *_PyDataMem_eventhook_user_data = NULL; /*NUMPY_API * Sets the allocation event hook for numpy array data. @@ -325,12 +325,6 @@ PyDataMem_RENEW(void *ptr, size_t size) return result; } -typedef void *(alloc_wrapper)(size_t, PyDataMem_AllocFunc *); -typedef void *(zalloc_wrapper)(size_t nelems, size_t elsize); -typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); -typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); -typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); - /* Memory handler global default */ static PyDataMem_Handler default_allocator = { "default_allocator", @@ -401,7 +395,8 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMem_FreeFunc *func) { if (func == npy_free_cache) { // All the logic below is conditionally handled by npy_free_cache - return npy_free_cache(ptr, size); + npy_free_cache(ptr, size); + return; } PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); func(ptr, size); From de22327d7adf121b9210a3abb46ed2f1cf8c1d1d Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Mon, 19 Oct 2020 22:47:30 +0300 Subject: [PATCH 06/76] MAINT: changes from review --- doc/source/reference/c-api/data_memory.rst | 60 ++++++++++++++++------ numpy/core/include/numpy/ndarraytypes.h | 2 +- numpy/core/src/multiarray/alloc.c | 2 +- numpy/core/src/multiarray/alloc.h | 1 + numpy/core/src/multiarray/ctors.c | 6 ++- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index 9023329cbdd0..df55305e2188 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -1,24 +1,52 @@ -Memory management ------------------ +Memory management in NumPy +========================== -The `numpy.ndarray` is a python class. It requires additinal memory allocations +The `numpy.ndarray` is a python class. It requires additional memory allocations to hold `numpy.ndarray.strides`, `numpy.ndarray.shape` and -``numpy.ndarray.data`` attributes. These attributes are specially allocated +`numpy.ndarray.data` attributes. These attributes are specially allocated after creating the python object in `__new__`. The ``strides`` and -``dimensions`` are stored in a piece of memory allocated internally. +``shape`` are stored in a piece of memory allocated internally. These allocations are small relative to the ``data``, the homogeneous chunk of memory used to store the actual array values (which could be pointers in the -case of ``object`` arrays). Users may wish to override the internal data -memory routines with ones of their own. They can do this by using -``PyDataMem_SetHandler``, which uses a ``PyDataMem_Handler`` structure to hold -pointers to functions used to manage the data memory. The calls are wrapped -by internal routines to call :c:func:`PyTraceMalloc_Track`, -:c:func:`PyTraceMalloc_Untrack`, and will use the `PyDataMem_EventHookFunc` -mechanism. Since the functions may change during the lifetime of the process, -each `ndarray` carries with it the functions used at the time of its -instantiation, and these will be used to reallocate or free the data memory of -the instance. +case of ``object`` arrays). Since that memory can be significantly large, NumPy +has provided interfaces to manage it. This document details how those +interfaces work. + +Historical overview +------------------- + +Since version 1.7.0, NumPy has exposed a set of ``PyDataMem_*`` functions +(:c:func:`PyDataMem_NEW`, :c:func:`PyDataMem_FREE`, :c:func:`PyDataMem_RENEW`) +which are backed by `alloc`, `free`, `realloc` respectively. In that version +NumPy also exposed the `PyDataMem_EventHook` function described below, which +wrap the OS-level calls. + +Python also improved its memory management capabilities, and began providing +various :ref:`management policies ` beginning in version +3.4. These routines are divided into a set of domains, each domain has a +:c:type:`PyMemAllocatorEx` structure of routines for memory management. Python also +added a `tracemalloc` module to trace calls to the various routines. These +tracking hooks were added to the NumPy ``PyDataMem_*`` routines. + +Configurable memory routines in NumPy +------------------------------------- + +Users may wish to override the internal data memory routines with ones of their +own. Since NumPy does not use the Python domain strategy to manage data memory, +it provides an alternative set of C-APIs to change memory routines. There are +no Python domain-wide strategies for large chunks of object data, so those are +less suited to NumPy's needs. User who wish to change the NumPy data memory +management routines can use :c:func:`PyDataMem_SetHandler`, which uses a +:c:type:`PyDataMem_Handler` structure to hold pointers to functions used to +manage the data memory. The calls are still wrapped by internal routines to +call :c:func:`PyTraceMalloc_Track`, :c:func:`PyTraceMalloc_Untrack`, and will +use the :c:func:`PyDataMem_EventHookFunc` mechanism. Since the functions may +change during the lifetime of the process, each ``ndarray`` carries with it the +functions used at the time of its instantiation, and these will be used to +reallocate or free the data memory of the instance. As of NumPy version 1.20, +the copy functions are not yet implemented, all memory copies are handled by +calls to ``memcpy``. .. c:type:: PyDataMem_Handler @@ -62,7 +90,7 @@ the instance. will be used to allocate data for the next PyArrayObject For an example of setting up and using the PyDataMem_Handler, see the test in -``numpy/core/tests/test_mem_policy.py`` +:file:`numpy/core/tests/test_mem_policy.py` .. c:function:: typedef void (PyDataMem_EventHookFunc)(void *inp, void *outp, size_t size, void *user_data); diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index 31b09c371c78..c91abe4e8a45 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -674,7 +674,7 @@ typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); typedef struct { - char name[128]; /* multiple of 64 to keep the struct unaligned */ + char name[128]; /* multiple of 64 to keep the struct aligned */ PyDataMem_AllocFunc *alloc; PyDataMem_ZeroedAllocFunc *zeroed_alloc; PyDataMem_FreeFunc *free; diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index c123be1529e4..4cf3bb60d768 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -326,7 +326,7 @@ PyDataMem_RENEW(void *ptr, size_t size) } /* Memory handler global default */ -static PyDataMem_Handler default_allocator = { +PyDataMem_Handler default_allocator = { "default_allocator", npy_alloc_cache, /* alloc */ npy_alloc_cache_zero, /* zeroed_alloc */ diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index 5a1726090358..c8aa84947212 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -40,6 +40,7 @@ npy_free_cache_dim_array(PyArrayObject * arr) } extern PyDataMem_Handler *current_allocator; +extern PyDataMem_Handler default_allocator; #define PyArray_HANDLER(arr) ((PyArrayObject_fields*)(arr))->mem_handler diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index f4161bfb0689..b490a4d30f26 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -804,10 +804,10 @@ PyArray_NewFromDescr_int( fa->flags |= NPY_ARRAY_C_CONTIGUOUS|NPY_ARRAY_F_CONTIGUOUS; } - /* Store the functions in case the global hander is modified */ - fa->mem_handler = current_allocator; if (data == NULL) { + /* Store the functions in case the global hander is modified */ + fa->mem_handler = current_allocator; /* * Allocate something even for zero-space arrays * e.g. shape=(0,) -- otherwise buffer exposure @@ -836,6 +836,8 @@ PyArray_NewFromDescr_int( fa->flags |= NPY_ARRAY_OWNDATA; } else { + /* The handlers should never be called in this case, but just in case */ + fa->mem_handler = &default_allocator; /* * If data is passed in, this object won't own it by default. * Caller must arrange for this to be reset if truly desired From 38274a49491f7ac93b32795b1842770024e4f7d7 Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 11 Mar 2021 21:41:15 +0200 Subject: [PATCH 07/76] fixes from linter --- doc/TESTS.rst.txt | 2 +- doc/source/reference/c-api/data_memory.rst | 2 +- numpy/core/code_generators/cversions.txt | 2 +- numpy/core/code_generators/numpy_api.py | 8 ++--- numpy/core/tests/test_mem_policy.py | 39 ++++++++++----------- numpy/testing/_private/extbuild.py | 40 +++++++++++++--------- test_requirements.txt | 2 +- tools/lint_diff.ini | 4 ++- 8 files changed, 53 insertions(+), 46 deletions(-) diff --git a/doc/TESTS.rst.txt b/doc/TESTS.rst.txt index 0b9cc610eb1e..06b34138c845 100644 --- a/doc/TESTS.rst.txt +++ b/doc/TESTS.rst.txt @@ -153,7 +153,7 @@ modules written "as-if" they know nothing about the internals of NumPy, rather using the official C-API interfaces only. Examples of such modules are tests for a user-defined ``rational`` dtype in ``_rational_tests`` or the ufunc machinery tests in ``_umath_tests`` which are part of the binary distribution. -Starting from version 1.20, you can also write snippets of C code in tests that +Starting from version 1.21, you can also write snippets of C code in tests that will be compiled locally into c-extension modules and loaded into python. .. currentmodule:: numpy.testing.extbuild diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index df55305e2188..bbeba10aaa4a 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -44,7 +44,7 @@ call :c:func:`PyTraceMalloc_Track`, :c:func:`PyTraceMalloc_Untrack`, and will use the :c:func:`PyDataMem_EventHookFunc` mechanism. Since the functions may change during the lifetime of the process, each ``ndarray`` carries with it the functions used at the time of its instantiation, and these will be used to -reallocate or free the data memory of the instance. As of NumPy version 1.20, +reallocate or free the data memory of the instance. As of NumPy version 1.21, the copy functions are not yet implemented, all memory copies are handled by calls to ``memcpy``. diff --git a/numpy/core/code_generators/cversions.txt b/numpy/core/code_generators/cversions.txt index 4a2e68525612..c632820054dd 100644 --- a/numpy/core/code_generators/cversions.txt +++ b/numpy/core/code_generators/cversions.txt @@ -59,5 +59,5 @@ # Version 14 (NumPy 1.22) No change. 0x0000000e = 17a0f366e55ec05e5c5c149123478452 -# Version 15 (NumPy 1.20) Configurable memory allocations +# Version 15 (NumPy 1.21) Configurable memory allocations 0x0000000f = 4177738910303368a00be8b1ce9283d5 diff --git a/numpy/core/code_generators/numpy_api.py b/numpy/core/code_generators/numpy_api.py index a915bbb35b5f..59ae7c592d25 100644 --- a/numpy/core/code_generators/numpy_api.py +++ b/numpy/core/code_generators/numpy_api.py @@ -76,9 +76,9 @@ # End 1.6 API } -#define NPY_NUMUSERTYPES (*(int *)PyArray_API[6]) -#define PyBoolArrType_Type (*(PyTypeObject *)PyArray_API[7]) -#define _PyArrayScalar_BoolValues ((PyBoolScalarObject *)PyArray_API[8]) +# define NPY_NUMUSERTYPES (*(int *)PyArray_API[6]) +# define PyBoolArrType_Type (*(PyTypeObject *)PyArray_API[7]) +# define _PyArrayScalar_BoolValues ((PyBoolScalarObject *)PyArray_API[8]) multiarray_funcs_api = { 'PyArray_GetNDArrayCVersion': (0,), @@ -352,7 +352,7 @@ # End 1.14 API 'PyDataMem_SetHandler': (304,), 'PyDataMem_GetHandlerName': (305,), - # End 1.20 API + # End 1.21 API } ufunc_types_api = { diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index f6320f66410c..d6e7c03b05ab 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -1,9 +1,8 @@ -import pathlib import pytest -import tempfile import numpy as np from numpy.testing import extbuild + @pytest.fixture def get_module(tmp_path): """ Add a memory policy that returns a false pointer 64 bytes into the @@ -18,21 +17,22 @@ def get_module(tmp_path): PyErr_SetString(PyExc_ValueError, "must be called with a numpy scalar or ndarray"); } - return PyUnicode_FromString(PyDataMem_GetHandlerName((PyArrayObject*)args)); + return PyUnicode_FromString( + PyDataMem_GetHandlerName((PyArrayObject*)args)); """ ), ("set_new_policy", "METH_NOARGS", - """ - const PyDataMem_Handler *old = PyDataMem_SetHandler(&new_handler); - return PyUnicode_FromString(old->name); - """), + """ + const PyDataMem_Handler *old = PyDataMem_SetHandler(&new_handler); + return PyUnicode_FromString(old->name); + """), ("set_old_policy", "METH_NOARGS", - """ - const PyDataMem_Handler *old = PyDataMem_SetHandler(NULL); - return PyUnicode_FromString(old->name); - """), + """ + const PyDataMem_Handler *old = PyDataMem_SetHandler(NULL); + return PyUnicode_FromString(old->name); + """), ] - prologue=''' + prologue = ''' #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include NPY_NO_EXPORT void * @@ -99,20 +99,18 @@ def get_module(tmp_path): memcpy, /* obj2obj */ }; ''' - more_init="import_array();" + more_init = "import_array();" try: import mem_policy return mem_policy except ImportError: pass # if it does not exist, build and load it - try: - return extbuild.build_and_import_extension('mem_policy', - functions, prologue=prologue, include_dirs=[np.get_include()], - build_dir=tmp_path, more_init=more_init) - except: - raise - pytest.skip("could not build module") + return extbuild.build_and_import_extension( + 'mem_policy', + functions, prologue=prologue, include_dirs=[np.get_include()], + build_dir=tmp_path, more_init=more_init + ) def test_set_policy(get_module): @@ -122,6 +120,7 @@ def test_set_policy(get_module): if orig_policy == 'default_allocator': get_module.set_old_policy() + @pytest.mark.slow def test_new_policy(get_module): a = np.arange(10) diff --git a/numpy/testing/_private/extbuild.py b/numpy/testing/_private/extbuild.py index a94ca94c1821..53f819be7cc0 100644 --- a/numpy/testing/_private/extbuild.py +++ b/numpy/testing/_private/extbuild.py @@ -7,17 +7,18 @@ import os import pathlib import sys -import setuptools +import setuptools # noqa import sysconfig from distutils.ccompiler import new_compiler from distutils.sysconfig import customize_compiler from distutils.errors import CompileError -__all__ = ['build_and_import_extensions', 'compile_extension_module'] +__all__ = ['build_and_import_extension', 'compile_extension_module'] -def build_and_import_extension(modname, functions, *, prologue="", build_dir=None, - include_dirs=[], more_init="", - ): + +def build_and_import_extension( + modname, functions, *, prologue="", build_dir=None, + include_dirs=[], more_init=""): """ Build and imports a c-extension module `modname` from a list of function fragments `functions`. @@ -54,7 +55,7 @@ def build_and_import_extension(modname, functions, *, prologue="", build_dir=Non >>> assert not mod.test_bytes(u'abc') >>> assert mod.test_bytes(b'abc') """ - + body = prologue + _make_methods(functions, modname) init = """PyObject *mod = PyModule_Create(&moduledef); """ @@ -78,7 +79,9 @@ def build_and_import_extension(modname, functions, *, prologue="", build_dir=Non spec.loader.exec_module(foo) return foo -def compile_extension_module(name, builddir, include_dirs, + +def compile_extension_module( + name, builddir, include_dirs, source_string, libraries=[], library_dirs=[]): """ Build an extension module and return the filename of the resulting @@ -104,10 +107,12 @@ def compile_extension_module(name, builddir, include_dirs, cfile = _convert_str_to_file(source_string, dirname) include_dirs = [sysconfig.get_config_var('INCLUDEPY')] + include_dirs - return _c_compile(cfile, outputfilename= dirname / modname, + return _c_compile( + cfile, outputfilename=dirname / modname, include_dirs=include_dirs, libraries=[], library_dirs=[], ) + def _convert_str_to_file(source, dirname): """Helper function to create a file ``source.c`` in `dirname` that contains the string in `source`. Returns the file name @@ -157,6 +162,7 @@ def _make_methods(functions, modname): """ % dict(methods='\n'.join(methods_table), modname=modname) return body + def _make_source(name, init, body): """ Combines the code fragments into source code ready to be compiled """ @@ -213,20 +219,24 @@ def _c_compile(cfile, outputfilename, include_dirs=[], libraries=[], os.environ[key] = value return outputfilename + def build(cfile, outputfilename, compile_extra, link_extra, - include_dirs, libraries, library_dirs): + include_dirs, libraries, library_dirs): "cd into the directory where the cfile is, use distutils to build" compiler = new_compiler(force=1) compiler.verbose = 1 customize_compiler(compiler) objects = [] - + old = os.getcwd() os.chdir(cfile.parent) try: - res = compiler.compile([str(cfile.name)], - include_dirs=include_dirs, extra_preargs=compile_extra) + res = compiler.compile( + [str(cfile.name)], + include_dirs=include_dirs, + extra_preargs=compile_extra + ) objects += [str(cfile.parent / r) for r in res] finally: os.chdir(old) @@ -237,12 +247,8 @@ def build(cfile, outputfilename, compile_extra, link_extra, extra_preargs=link_extra, library_dirs=library_dirs) + def get_so_suffix(): ret = sysconfig.get_config_var('EXT_SUFFIX') assert ret return ret - -def get_extbuilder(base_dir): - return ExtensionCompiler( - builddir_base=base_dir, - ) diff --git a/test_requirements.txt b/test_requirements.txt index 9f2ee2001644..65b79fe52b8c 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,6 +1,6 @@ cython==0.29.24 wheel<0.36.3 -setuptools<49.2.0 +setuptools >=41.6.0,<49.2.0 hypothesis==6.14.5 pytest==6.2.4 pytz==2021.1 diff --git a/tools/lint_diff.ini b/tools/lint_diff.ini index 3b66d3c3e900..a8f7f21bab8f 100644 --- a/tools/lint_diff.ini +++ b/tools/lint_diff.ini @@ -1,4 +1,6 @@ [pycodestyle] max_line_length = 79 statistics = True -ignore = E121,E122,E123,E125,E126,E127,E128,E226,E251,E265,E266,E302,E402,E704,E712,E721,E731,E741,W291,W293,W391,W503,W504 +ignore = E121,E122,E123,E125,E126,E127,E128,E226,E241,E251,E265,E266,E302,E402,E704,E712,E721,E731,E741,W291,W293,W391,W503,W504 +exclude = numpy/__config__.py,numpy/typing/tests/data +>>>>>>> fixes from linter From 59b520a2e928ad15848f4f6a39ce3539395186d1 Mon Sep 17 00:00:00 2001 From: mattip Date: Fri, 16 Apr 2021 11:17:11 -0400 Subject: [PATCH 08/76] setting ndarray->descr on 0d or scalars mess with FREE --- numpy/core/src/multiarray/arrayobject.c | 11 +++++-- numpy/core/src/multiarray/ctors.c | 1 + numpy/core/src/multiarray/getset.c | 11 ++++++- numpy/core/src/multiarray/methods.c | 19 +++++++++++-- numpy/core/tests/test_mem_policy.py | 38 ++++++++++++++++++------- numpy/ma/mrecords.py | 1 - 6 files changed, 65 insertions(+), 16 deletions(-) diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index b439c11374fd..14d8a0d9b49b 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -493,8 +493,15 @@ array_dealloc(PyArrayObject *self) if (PyDataType_FLAGCHK(fa->descr, NPY_ITEM_REFCOUNT)) { PyArray_XDECREF(self); } - PyDataMem_UserFREE(fa->data, PyArray_NBYTES(self), - fa->mem_handler->free); + /* + * Allocation will never be 0, see comment in ctors.c + * line 820 + */ + size_t nbytes = PyArray_NBYTES(self); + if (nbytes == 0) { + nbytes = fa->descr->elsize ? fa->descr->elsize : 1; + } + PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler->free); } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index b490a4d30f26..92c15872d413 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -813,6 +813,7 @@ PyArray_NewFromDescr_int( * e.g. shape=(0,) -- otherwise buffer exposure * (a.data) doesn't work as it should. * Could probably just allocate a few bytes here. -- Chuck + * Note: always sync this with calls to PyDataMem_UserFREE */ if (nbytes == 0) { nbytes = descr->elsize ? descr->elsize : 1; diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index c2ced1b305f2..c55a0778e399 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -384,7 +384,16 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) } if (PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA) { PyArray_XDECREF(self); - PyDataMem_UserFREE(PyArray_DATA(self), PyArray_NBYTES(self), + size_t nbytes = PyArray_NBYTES(self); + /* + * Allocation will never be 0, see comment in ctors.c + * line 820 + */ + if (nbytes == 0) { + PyArray_Descr *dtype = PyArray_DESCR(self); + nbytes = dtype->elsize ? dtype->elsize : 1; + } + PyDataMem_UserFREE(PyArray_DATA(self), nbytes, PyArray_HANDLER(self)->free); } if (PyArray_BASE(self)) { diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 008d51411eec..7cb23d086860 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -1973,6 +1973,16 @@ array_setstate(PyArrayObject *self, PyObject *args) return NULL; } + /* + * Reassigning fa->descr messes with the reallocation strategy, + * since fa could be a 0-d or scalar, and then + * PyDataMem_UserFREE will be confused + */ + size_t n_tofree = PyArray_NBYTES(self); + if (n_tofree == 0) { + PyArray_Descr *dtype = PyArray_DESCR(self); + n_tofree = dtype->elsize ? dtype->elsize : 1; + } Py_XDECREF(PyArray_DESCR(self)); fa->descr = typecode; Py_INCREF(typecode); @@ -2039,7 +2049,11 @@ array_setstate(PyArrayObject *self, PyObject *args) } if ((PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA)) { - PyDataMem_UserFREE(PyArray_DATA(self), PyArray_NBYTES(self), + /* + * Allocation will never be 0, see comment in ctors.c + * line 820 + */ + PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, PyArray_HANDLER(self)->free); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } @@ -2076,7 +2090,6 @@ array_setstate(PyArrayObject *self, PyObject *args) if (!PyDataType_FLAGCHK(typecode, NPY_LIST_PICKLE)) { int swap = PyArray_ISBYTESWAPPED(self); - fa->data = datastr; /* Bytes should always be considered immutable, but we just grab the * pointer if they are large, to save memory. */ if (!IsAligned(self) || swap || (len <= 1000)) { @@ -2122,7 +2135,9 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); } else { + fa->data = datastr; if (PyArray_SetBaseObject(self, rawdata) < 0) { + Py_DECREF(rawdata); return NULL; } } diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index d6e7c03b05ab..6fa80a4da524 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -41,7 +41,7 @@ def get_module(tmp_path): if (real == NULL) { return NULL; } - snprintf(real, 64, "originally allocated %ld", sz); + snprintf(real, 64, "originally allocated %ld", (unsigned long)sz); return (void *)(real + 64); } NPY_NO_EXPORT void * @@ -50,7 +50,8 @@ def get_module(tmp_path): if (real == NULL) { return NULL; } - snprintf(real, 64, "originally allocated %ld", sz); + snprintf(real, 64, "originally allocated %ld via zero", + (unsigned long)sz); return (void *)(real + 64); } NPY_NO_EXPORT void @@ -60,13 +61,24 @@ def get_module(tmp_path): } char *real = (char *)p - 64; if (strncmp(real, "originally allocated", 20) != 0) { - fprintf(stdout, "uh-oh, unmatched shift_free\\n"); + fprintf(stdout, "uh-oh, unmatched shift_free, " + "no appropriate prefix\\n"); /* Make gcc crash by calling free on the wrong address */ free((char *)p + 10); - /* free(p); */ + /* free(real); */ } else { - free(real); + int i = atoi(real +20); + if (i != sz) { + fprintf(stderr, "uh-oh, unmatched " + "shift_free(ptr, %d) but allocated %d\\n", sz, i); + /* Make gcc crash by calling free on the wrong address */ + /* free((char *)p + 10); */ + free(real); + } + else { + free(real); + } } } NPY_NO_EXPORT void * @@ -84,7 +96,8 @@ def get_module(tmp_path): if (real == NULL) { return NULL; } - snprintf(real, 64, "originally allocated (realloc) %ld", sz); + snprintf(real, 64, "originally allocated " + "%ld via realloc", (unsigned long)sz); return (void *)(real + 64); } } @@ -131,11 +144,16 @@ def test_new_policy(get_module): # test array manipulation. This is slow if orig_policy == 'default_allocator': - # when the test set recurses into this test, the policy will be set - # so this "if" will be false, preventing infinite recursion + # when the np.core.test tests recurse into this test, the + # policy will be set so this "if" will be false, preventing + # infinite recursion # - # if needed, debug this by setting extra_argv=['-vvx'] - np.core.test(verbose=0, extra_argv=[]) + # if needed, debug this by + # - running tests with -- -s (to not capture stdout/stderr + # - setting extra_argv=['-vv'] here + np.core.test(verbose=2, extra_argv=['-vv']) + # also try the ma tests, the pickling test is quite tricky + np.ma.test(verbose=2, extra_argv=['-vv']) get_module.set_old_policy() assert get_module.test_prefix(a) == orig_policy c = np.arange(10) diff --git a/numpy/ma/mrecords.py b/numpy/ma/mrecords.py index 6814931b0b30..c527a7276329 100644 --- a/numpy/ma/mrecords.py +++ b/numpy/ma/mrecords.py @@ -493,7 +493,6 @@ def _mrreconstruct(subtype, baseclass, baseshape, basetype,): _mask = ndarray.__new__(ndarray, baseshape, 'b1') return subtype.__new__(subtype, _data, mask=_mask, dtype=basetype,) - mrecarray = MaskedRecords From 5264019f7dcab21c3a8cc8a02fc9383f6b6e8046 Mon Sep 17 00:00:00 2001 From: mattip Date: Fri, 16 Apr 2021 14:10:58 -0400 Subject: [PATCH 09/76] make scalar allocation more consistent wrt np_alloc_cache --- numpy/core/src/multiarray/scalartypes.c.src | 3 +++ 1 file changed, 3 insertions(+) diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index 2d581845eb98..bb5277f7cfde 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -3032,6 +3032,9 @@ void_arrtype_new(PyTypeObject *type, PyObject *args, PyObject *kwds) (int) NPY_MAX_INT); return NULL; } + if (memu == 0) { + memu = 1; + } destptr = npy_alloc_cache_zero(memu, 1); if (destptr == NULL) { return PyErr_NoMemory(); From 7c396d72909083f3ab313e9a224fd12266ae5ebf Mon Sep 17 00:00:00 2001 From: mattip Date: Fri, 16 Apr 2021 23:31:44 -0400 Subject: [PATCH 10/76] change formatting for sphinx --- doc/source/reference/c-api/data_memory.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index bbeba10aaa4a..bd87f432e75e 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -92,16 +92,14 @@ calls to ``memcpy``. For an example of setting up and using the PyDataMem_Handler, see the test in :file:`numpy/core/tests/test_mem_policy.py` -.. c:function:: typedef void (PyDataMem_EventHookFunc)(void *inp, void *outp, - size_t size, void *user_data); +.. c:function:: void PyDataMem_EventHookFunc(void *inp, void *outp, size_t size, void *user_data); This function will be called on NEW,FREE,RENEW calls in data memory manipulation -.. c:function:: PyDataMem_EventHookFunc * PyDataMem_SetEventHook( - PyDataMem_EventHookFunc *newhook, void *user_data, void **old_data) +.. c:function:: PyDataMem_EventHookFunc * PyDataMem_SetEventHook(PyDataMem_EventHookFunc *newhook, void *user_data, void **old_data) Sets the allocation event hook for numpy array data. From de9001c28aaedbd54a5c95c3531020cafff18d7d Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 19 Apr 2021 19:21:34 -0400 Subject: [PATCH 11/76] remove memcpy variants --- numpy/core/include/numpy/ndarraytypes.h | 3 --- numpy/core/src/multiarray/alloc.c | 5 +---- numpy/core/tests/test_mem_policy.py | 5 +---- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index c91abe4e8a45..a6da4d26f27b 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -679,9 +679,6 @@ typedef struct { PyDataMem_ZeroedAllocFunc *zeroed_alloc; PyDataMem_FreeFunc *free; PyDataMem_ReallocFunc *realloc; - PyDataMem_CopyFunc *host2obj; /* copy from the host python */ - PyDataMem_CopyFunc *obj2host; /* copy to the host python */ - PyDataMem_CopyFunc *obj2obj; /* copy between two objects */ } PyDataMem_Handler; diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 4cf3bb60d768..7b5d7e0704b5 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -331,10 +331,7 @@ PyDataMem_Handler default_allocator = { npy_alloc_cache, /* alloc */ npy_alloc_cache_zero, /* zeroed_alloc */ npy_free_cache, /* free */ - PyDataMem_RENEW, /* realloc */ - memcpy, /* host2obj */ - memcpy, /* obj2host */ - memcpy, /* obj2obj */ + PyDataMem_RENEW /* realloc */ }; PyDataMem_Handler *current_allocator = &default_allocator; diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 6fa80a4da524..1c6d75b58146 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -106,10 +106,7 @@ def get_module(tmp_path): shift_alloc, /* alloc */ shift_zero, /* zeroed_alloc */ shift_free, /* free */ - shift_realloc, /* realloc */ - memcpy, /* host2obj */ - memcpy, /* obj2host */ - memcpy, /* obj2obj */ + shift_realloc /* realloc */ }; ''' more_init = "import_array();" From 9e7c3ed40bcf51a0cdbc46dc1dfcb9550bded991 Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 3 May 2021 21:18:05 +0300 Subject: [PATCH 12/76] update to match NEP 49 --- doc/source/reference/c-api/data_memory.rst | 52 +++++++++++----------- numpy/core/include/numpy/ndarraytypes.h | 1 - 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index bd87f432e75e..7507c52af8a7 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -7,11 +7,10 @@ to hold `numpy.ndarray.strides`, `numpy.ndarray.shape` and after creating the python object in `__new__`. The ``strides`` and ``shape`` are stored in a piece of memory allocated internally. -These allocations are small relative to the ``data``, the homogeneous chunk of -memory used to store the actual array values (which could be pointers in the -case of ``object`` arrays). Since that memory can be significantly large, NumPy -has provided interfaces to manage it. This document details how those -interfaces work. +The ``data`` allocation used to store the actual array values (which could be +pointers in the case of ``object`` arrays) can be very large, so NumPy has +provided interfaces to manage its allocation and release. This document details +how those interfaces work. Historical overview ------------------- @@ -22,15 +21,23 @@ which are backed by `alloc`, `free`, `realloc` respectively. In that version NumPy also exposed the `PyDataMem_EventHook` function described below, which wrap the OS-level calls. -Python also improved its memory management capabilities, and began providing +Since those early days, Python also improved its memory management +capabilities, and began providing various :ref:`management policies ` beginning in version 3.4. These routines are divided into a set of domains, each domain has a :c:type:`PyMemAllocatorEx` structure of routines for memory management. Python also added a `tracemalloc` module to trace calls to the various routines. These tracking hooks were added to the NumPy ``PyDataMem_*`` routines. -Configurable memory routines in NumPy -------------------------------------- +NumPy added a small cache of allocated memory in its internal +``npy_alloc_cache``, ``npy_alloc_cache_zero``, and ``npy_free_cache`` +functions. These wrap ``alloc``, ``alloc-and-memset(0)`` and ``free`` +respectively, but when ``npy_free_cache`` is called, it adds the pointer to a +short list of available blocks marked by size. These blocks can be re-used by +subsequent calls to ``npy_alloc*``, avoiding memory thrashing. + +Configurable memory routines in NumPy (NEP 49) +---------------------------------------------- Users may wish to override the internal data memory routines with ones of their own. Since NumPy does not use the Python domain strategy to manage data memory, @@ -44,9 +51,7 @@ call :c:func:`PyTraceMalloc_Track`, :c:func:`PyTraceMalloc_Untrack`, and will use the :c:func:`PyDataMem_EventHookFunc` mechanism. Since the functions may change during the lifetime of the process, each ``ndarray`` carries with it the functions used at the time of its instantiation, and these will be used to -reallocate or free the data memory of the instance. As of NumPy version 1.21, -the copy functions are not yet implemented, all memory copies are handled by -calls to ``memcpy``. +reallocate or free the data memory of the instance. .. c:type:: PyDataMem_Handler @@ -55,14 +60,11 @@ calls to ``memcpy``. .. code-block:: c typedef struct { - char name[128]; /* multiple of 64 to keep the struct unaligned */ + char name[128]; /* multiple of 64 to keep the struct aligned */ PyDataMem_AllocFunc *alloc; PyDataMem_ZeroedAllocFunc *zeroed_alloc; PyDataMem_FreeFunc *free; PyDataMem_ReallocFunc *realloc; - PyDataMem_CopyFunc *host2obj; /* copy from the host python */ - PyDataMem_CopyFunc *obj2host; /* copy to the host python */ - PyDataMem_CopyFunc *obj2obj; /* copy between two objects */ } PyDataMem_Handler; where the function's signatures are @@ -73,29 +75,27 @@ calls to ``memcpy``. typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); - typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); .. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) - Sets a new allocation policy. If the input value is NULL, will reset - the policy to the default. Returns the previous policy, NULL if the + Sets a new allocation policy. If the input value is ``NULL``, will reset + the policy to the default. Returns the previous policy, ``NULL`` if the previous policy was the default. We wrap the user-provided functions so they will still call the python and numpy memory management callback hooks. .. c:function:: const char * PyDataMem_GetHandlerName(PyArrayObject *obj) - Return the const char name of the PyDataMem_Handler used by the - PyArrayObject. If NULL, return the name of the current global policy that - will be used to allocate data for the next PyArrayObject + Return the const char name of the `PyDataMem_Handler` used by the + ``PyArrayObject``. If ``NULL``, return the name of the current global policy + that will be used to allocate data for the next ``PyArrayObject``. For an example of setting up and using the PyDataMem_Handler, see the test in :file:`numpy/core/tests/test_mem_policy.py` .. c:function:: void PyDataMem_EventHookFunc(void *inp, void *outp, size_t size, void *user_data); - This function will be called on NEW,FREE,RENEW calls in data memory - manipulation + This function will be called during data memory manipulation @@ -103,10 +103,10 @@ For an example of setting up and using the PyDataMem_Handler, see the test in Sets the allocation event hook for numpy array data. - Returns a pointer to the previous hook or NULL. If old_data is - non-NULL, the previous user_data pointer will be copied to it. + Returns a pointer to the previous hook or ``NULL``. If old_data is + non-``NULL``, the previous user_data pointer will be copied to it. - If not NULL, hook will be called at the end of each PyDataMem_NEW/FREE/RENEW: + If not ``NULL``, hook will be called at the end of each ``PyDataMem_NEW/FREE/RENEW``: .. code-block:: c diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index a6da4d26f27b..b12332556f05 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -671,7 +671,6 @@ typedef void *(PyDataMem_AllocFunc)(size_t size); typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); -typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); typedef struct { char name[128]; /* multiple of 64 to keep the struct aligned */ From 953cc8883f2647a7aa9c7fb6e5eb0472d7f8c140 Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 6 May 2021 14:29:33 +0300 Subject: [PATCH 13/76] ENH: add a python-level get_handler_name --- numpy/core/multiarray.py | 4 ++-- numpy/core/src/multiarray/alloc.c | 11 +++++++++++ numpy/core/src/multiarray/multiarraymodule.c | 3 +++ numpy/core/tests/test_mem_policy.py | 6 ++++-- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/numpy/core/multiarray.py b/numpy/core/multiarray.py index 154df6f4d7da..351cd3a1bbb5 100644 --- a/numpy/core/multiarray.py +++ b/numpy/core/multiarray.py @@ -31,8 +31,8 @@ 'count_nonzero', 'c_einsum', 'datetime_as_string', 'datetime_data', 'dot', 'dragon4_positional', 'dragon4_scientific', 'dtype', 'empty', 'empty_like', 'error', 'flagsobj', 'flatiter', 'format_longfloat', - 'frombuffer', 'fromfile', 'fromiter', 'fromstring', 'inner', - 'interp', 'interp_complex', 'is_busday', 'lexsort', + 'frombuffer', 'fromfile', 'fromiter', 'fromstring', 'get_handler_name', + 'inner', 'interp', 'interp_complex', 'is_busday', 'lexsort', 'matmul', 'may_share_memory', 'min_scalar_type', 'ndarray', 'nditer', 'nested_iters', 'normalize_axis_index', 'packbits', 'promote_types', 'putmask', 'ravel_multi_index', 'result_type', 'scalar', diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 7b5d7e0704b5..4cb835dac077 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -464,3 +464,14 @@ PyDataMem_GetHandlerName(PyArrayObject *obj) } return PyArray_HANDLER(obj)->name; } + +NPY_NO_EXPORT PyObject * +get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj) +{ + const char * name = PyDataMem_GetHandlerName(obj); + if (name == NULL) { + return NULL; + } + if (! PyCheck + return PyUnicode_FromString(name); +} diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index ea9c10543469..fbe22b3b9512 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -4428,6 +4428,9 @@ static struct PyMethodDef array_module_methods[] = { {"geterrobj", (PyCFunction) ufunc_geterr, METH_VARARGS, NULL}, + {"get_handler_name", + (PyCFunction) get_handler_name, + METH_O, NULL}, {"_add_newdoc_ufunc", (PyCFunction)add_newdoc_ufunc, METH_VARARGS, NULL}, {"_get_sfloat_dtype", diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 1c6d75b58146..c76d37694d96 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -63,7 +63,7 @@ def get_module(tmp_path): if (strncmp(real, "originally allocated", 20) != 0) { fprintf(stdout, "uh-oh, unmatched shift_free, " "no appropriate prefix\\n"); - /* Make gcc crash by calling free on the wrong address */ + /* Make C runtime crash by calling free on the wrong address */ free((char *)p + 10); /* free(real); */ } @@ -72,7 +72,7 @@ def get_module(tmp_path): if (i != sz) { fprintf(stderr, "uh-oh, unmatched " "shift_free(ptr, %d) but allocated %d\\n", sz, i); - /* Make gcc crash by calling free on the wrong address */ + /* Make C runtime crash by calling free on the wrong address */ /* free((char *)p + 10); */ free(real); } @@ -126,6 +126,8 @@ def get_module(tmp_path): def test_set_policy(get_module): a = np.arange(10) orig_policy = get_module.test_prefix(a) + assert orig_policy == np.core.multiarray.get_handler_name() + assert orig_policy == np.core.multiarray.get_handler_name(a) assert get_module.set_new_policy() == orig_policy if orig_policy == 'default_allocator': get_module.set_old_policy() From ad9329b55ad98b367d6644ff82211f09a27c91a9 Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 6 May 2021 16:26:26 +0300 Subject: [PATCH 14/76] ENH: add core.multiarray.get_handler_name --- numpy/core/_add_newdocs.py | 9 +++++++++ numpy/core/src/multiarray/alloc.c | 13 ++++++++++--- numpy/core/src/multiarray/alloc.h | 4 ++++ numpy/core/src/multiarray/multiarraymodule.c | 2 +- numpy/core/tests/test_mem_policy.py | 9 ++++----- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py index 06f2a6376fac..d4b3d6712e36 100644 --- a/numpy/core/_add_newdocs.py +++ b/numpy/core/_add_newdocs.py @@ -4688,6 +4688,15 @@ and then throwing away the ufunc. """) +add_newdoc('numpy.core.multiarray', 'get_handler_name', + """ + get_handler_name(a: ndarray) -> str + + Return the name of the memory handler used by `a`. If not provided, return + the name of the current global memory handler that will be used to allocate + data for the next `ndarray`. + """) + add_newdoc('numpy.core.multiarray', '_set_madvise_hugepage', """ _set_madvise_hugepage(enabled: bool) -> bool diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 4cb835dac077..fd1b38e29d41 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -466,12 +466,19 @@ PyDataMem_GetHandlerName(PyArrayObject *obj) } NPY_NO_EXPORT PyObject * -get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj) +get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args) { - const char * name = PyDataMem_GetHandlerName(obj); + PyObject *arr=NULL; + if (!PyArg_ParseTuple(args, "|O:get_handler_name", &arr)) { + return NULL; + } + if (arr != NULL && !PyArray_Check(arr)) { + PyErr_SetString(PyExc_ValueError, "if supplied, argument must be an ndarray"); + return NULL; + } + const char * name = PyDataMem_GetHandlerName((PyArrayObject *)arr); if (name == NULL) { return NULL; } - if (! PyCheck return PyUnicode_FromString(name); } diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index c8aa84947212..c6a955ae4960 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -42,6 +42,10 @@ npy_free_cache_dim_array(PyArrayObject * arr) extern PyDataMem_Handler *current_allocator; extern PyDataMem_Handler default_allocator; +NPY_NO_EXPORT PyObject * +get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj); + + #define PyArray_HANDLER(arr) ((PyArrayObject_fields*)(arr))->mem_handler #endif diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index fbe22b3b9512..8db2666376c2 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -4430,7 +4430,7 @@ static struct PyMethodDef array_module_methods[] = { METH_VARARGS, NULL}, {"get_handler_name", (PyCFunction) get_handler_name, - METH_O, NULL}, + METH_VARARGS, NULL}, {"_add_newdoc_ufunc", (PyCFunction)add_newdoc_ufunc, METH_VARARGS, NULL}, {"_get_sfloat_dtype", diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index c76d37694d96..764dc0900d89 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -68,12 +68,11 @@ def get_module(tmp_path): /* free(real); */ } else { - int i = atoi(real +20); + npy_uintp i = (npy_uintp)atoi(real +20); if (i != sz) { - fprintf(stderr, "uh-oh, unmatched " - "shift_free(ptr, %d) but allocated %d\\n", sz, i); - /* Make C runtime crash by calling free on the wrong address */ - /* free((char *)p + 10); */ + fprintf(stderr, "uh-oh, unmatched shift_free" + "(ptr, %ld) but allocated %ld\\n", sz, i); + /* This happens in some places, only print */ free(real); } else { From 6d10fdb9e7785b1ea0bcc21ab3ed1435e89d5988 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 30 Jun 2021 16:23:33 +0300 Subject: [PATCH 15/76] Allow closure-like definition of the data mem routines --- doc/source/reference/c-api/data_memory.rst | 18 ++++---- numpy/core/include/numpy/ndarraytypes.h | 18 ++++---- numpy/core/src/multiarray/alloc.c | 50 +++++++++++++--------- numpy/core/src/multiarray/alloc.h | 12 +++--- numpy/core/src/multiarray/arrayobject.c | 2 +- numpy/core/src/multiarray/arraytypes.c.src | 10 ++--- numpy/core/src/multiarray/ctors.c | 18 ++++---- numpy/core/src/multiarray/getset.c | 2 +- numpy/core/src/multiarray/item_selection.c | 12 +++--- numpy/core/src/multiarray/methods.c | 6 +-- numpy/core/src/multiarray/shape.c | 2 +- numpy/core/tests/test_mem_policy.py | 47 +++++++++++++------- 12 files changed, 110 insertions(+), 87 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index 7507c52af8a7..ad143bd7d0c0 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -61,20 +61,20 @@ reallocate or free the data memory of the instance. typedef struct { char name[128]; /* multiple of 64 to keep the struct aligned */ - PyDataMem_AllocFunc *alloc; - PyDataMem_ZeroedAllocFunc *zeroed_alloc; - PyDataMem_FreeFunc *free; - PyDataMem_ReallocFunc *realloc; + PyDataMemAllocator allocator; } PyDataMem_Handler; - where the function's signatures are + where the allocator structure is: .. code-block:: c - typedef void *(PyDataMem_AllocFunc)(size_t size); - typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); - typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); - typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); + 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; .. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index b12332556f05..80313685a81c 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -667,17 +667,17 @@ typedef struct _arr_descr { /* * Memory handler structure for array data. */ -typedef void *(PyDataMem_AllocFunc)(size_t size); -typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); -typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); -typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); +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; typedef struct { char name[128]; /* multiple of 64 to keep the struct aligned */ - PyDataMem_AllocFunc *alloc; - PyDataMem_ZeroedAllocFunc *zeroed_alloc; - PyDataMem_FreeFunc *free; - PyDataMem_ReallocFunc *realloc; + PyDataMemAllocator allocator; } PyDataMem_Handler; @@ -732,7 +732,7 @@ typedef struct tagPyArrayObject_fields { PyObject *weakreflist; void *_buffer_info; /* private buffer info, tagged to allow warning */ /* - * For alloc/malloc/realloc/free/memcpy per object + * For malloc/calloc/realloc/free per object */ PyDataMem_Handler *mem_handler; } PyArrayObject_fields; diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index fd1b38e29d41..3d5ef9d96fc2 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -326,31 +326,34 @@ PyDataMem_RENEW(void *ptr, size_t size) } /* Memory handler global default */ -PyDataMem_Handler default_allocator = { +PyDataMem_Handler default_handler = { "default_allocator", - npy_alloc_cache, /* alloc */ - npy_alloc_cache_zero, /* zeroed_alloc */ - npy_free_cache, /* free */ - PyDataMem_RENEW /* realloc */ + { + NULL, /* ctx */ + npy_alloc_cache, /* malloc */ + npy_alloc_cache_zero, /* calloc */ + PyDataMem_RENEW, /* realloc */ + npy_free_cache /* free */ + } }; -PyDataMem_Handler *current_allocator = &default_allocator; +PyDataMem_Handler *current_handler = &default_handler; int uo_index=0; /* user_override index */ /* Wrappers for user-assigned PyDataMem_Handlers */ NPY_NO_EXPORT void * -PyDataMem_UserNEW(size_t size, PyDataMem_AllocFunc *alloc) +PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) { void *result; - if (alloc == npy_alloc_cache) { + if (allocator.malloc == npy_alloc_cache) { // All the logic below is conditionally handled by npy_alloc_cache return npy_alloc_cache(size); } assert(size != 0); - result = alloc(size); + result = allocator.malloc(allocator.ctx, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -365,15 +368,15 @@ PyDataMem_UserNEW(size_t size, PyDataMem_AllocFunc *alloc) } NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *zalloc) +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) { void *result; - if (zalloc == npy_alloc_cache_zero) { + if (allocator.calloc == npy_alloc_cache_zero) { // All the logic below is conditionally handled by npy_alloc_cache_zero return npy_alloc_cache_zero(nmemb, size); } - result = zalloc(nmemb, size); + result = allocator.calloc(allocator.ctx, nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -388,15 +391,15 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *z } NPY_NO_EXPORT void -PyDataMem_UserFREE(void *ptr, size_t size, PyDataMem_FreeFunc *func) +PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (func == npy_free_cache) { + if (allocator.free == npy_free_cache) { // All the logic below is conditionally handled by npy_free_cache npy_free_cache(ptr, size); return; } PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); - func(ptr, size); + allocator.free(allocator.ctx, ptr, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -409,12 +412,17 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMem_FreeFunc *func) } NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func) +PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) { + if (allocator.realloc == PyDataMem_RENEW) { + // All the logic below is conditionally handled by PyDataMem_RENEW + return PyDataMem_RENEW(ptr, size); + } + void *result; assert(size != 0); - result = func(ptr, size); + result = allocator.realloc(allocator.ctx, ptr, size); if (result != ptr) { PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); } @@ -441,12 +449,12 @@ PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func) NPY_NO_EXPORT const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) { - const PyDataMem_Handler *old = current_allocator; + const PyDataMem_Handler *old = current_handler; if (handler) { - current_allocator = handler; + current_handler = handler; } else { - current_allocator = &default_allocator; + current_handler = &default_handler; } return old; } @@ -460,7 +468,7 @@ NPY_NO_EXPORT const char * PyDataMem_GetHandlerName(PyArrayObject *obj) { if (obj == NULL) { - return current_allocator->name; + return current_handler->name; } return PyArray_HANDLER(obj)->name; } diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index c6a955ae4960..12a859b44e98 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -10,16 +10,16 @@ NPY_NO_EXPORT PyObject * _set_madvise_hugepage(PyObject *NPY_UNUSED(self), PyObject *enabled_obj); NPY_NO_EXPORT void * -PyDataMem_UserNEW(npy_uintp sz, PyDataMem_AllocFunc *alloc); +PyDataMem_UserNEW(npy_uintp sz, PyDataMemAllocator allocator); NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *zalloc); +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator); NPY_NO_EXPORT void -PyDataMem_UserFREE(void * p, npy_uintp sd, PyDataMem_FreeFunc *func); +PyDataMem_UserFREE(void * p, npy_uintp sd, PyDataMemAllocator allocator); NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func); +PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator); NPY_NO_EXPORT void * npy_alloc_cache_dim(npy_uintp sz); @@ -39,8 +39,8 @@ npy_free_cache_dim_array(PyArrayObject * arr) npy_free_cache_dim(PyArray_DIMS(arr), PyArray_NDIM(arr)); } -extern PyDataMem_Handler *current_allocator; -extern PyDataMem_Handler default_allocator; +extern PyDataMem_Handler *current_handler; +extern PyDataMem_Handler default_handler; NPY_NO_EXPORT PyObject * get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj); diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index 14d8a0d9b49b..926813cdbf75 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -501,7 +501,7 @@ array_dealloc(PyArrayObject *self) if (nbytes == 0) { nbytes = fa->descr->elsize ? fa->descr->elsize : 1; } - PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler->free); + PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler->allocator); } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 22c62c20d319..9529f2af745d 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3117,7 +3117,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (swap || new->alignment > 1) { if (swap || !npy_is_aligned(nip1, new->alignment)) { /* create buffer and copy */ - nip1 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->alloc); + nip1 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->allocator); if (nip1 == NULL) { goto finish; } @@ -3127,11 +3127,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } if (swap || !npy_is_aligned(nip2, new->alignment)) { /* create buffer and copy */ - nip2 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->alloc); + nip2 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->allocator); if (nip2 == NULL) { if (nip1 != ip1 + offset) { PyDataMem_UserFREE(nip1, new->elsize, - PyArray_HANDLER(ap)->free); + PyArray_HANDLER(ap)->allocator); } goto finish; } @@ -3143,10 +3143,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, dummy); if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { - PyDataMem_UserFREE(nip1, new->elsize, PyArray_HANDLER(ap)->free); + PyDataMem_UserFREE(nip1, new->elsize, PyArray_HANDLER(ap)->allocator); } if (nip2 != ip2 + offset) { - PyDataMem_UserFREE(nip2, new->elsize, PyArray_HANDLER(ap)->free); + PyDataMem_UserFREE(nip2, new->elsize, PyArray_HANDLER(ap)->allocator); } } if (res != 0) { diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 92c15872d413..5df1aa1146f3 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -807,7 +807,7 @@ PyArray_NewFromDescr_int( if (data == NULL) { /* Store the functions in case the global hander is modified */ - fa->mem_handler = current_allocator; + fa->mem_handler = current_handler; /* * Allocate something even for zero-space arrays * e.g. shape=(0,) -- otherwise buffer exposure @@ -824,10 +824,10 @@ PyArray_NewFromDescr_int( */ if (zeroed || PyDataType_FLAGCHK(descr, NPY_NEEDS_INIT)) { data = PyDataMem_UserNEW_ZEROED(nbytes, 1, - fa->mem_handler->zeroed_alloc); + fa->mem_handler->allocator); } else { - data = PyDataMem_UserNEW(nbytes, fa->mem_handler->alloc); + data = PyDataMem_UserNEW(nbytes, fa->mem_handler->allocator); } if (data == NULL) { raise_memory_error(fa->nd, fa->dimensions, descr); @@ -838,7 +838,7 @@ PyArray_NewFromDescr_int( } else { /* The handlers should never be called in this case, but just in case */ - fa->mem_handler = &default_allocator; + fa->mem_handler = &default_handler; /* * If data is passed in, this object won't own it by default. * Caller must arrange for this to be reset if truly desired @@ -3410,7 +3410,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre if (num < 0 && thisbuf == size) { totalbytes += bytes; tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes, - PyArray_HANDLER(r)->realloc); + PyArray_HANDLER(r)->allocator); if (tmp == NULL) { err = 1; break; @@ -3433,7 +3433,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre if (nsize != 0) { tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize, - PyArray_HANDLER(r)->realloc); + PyArray_HANDLER(r)->allocator); if (tmp == NULL) { err = 1; } @@ -3539,7 +3539,7 @@ PyArray_FromFile(FILE *fp, PyArray_Descr *dtype, npy_intp num, char *sep) char *tmp; if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize, - PyArray_HANDLER(ret)->realloc)) == NULL) { + PyArray_HANDLER(ret)->allocator)) == NULL) { Py_DECREF(dtype); Py_DECREF(ret); return PyErr_NoMemory(); @@ -3824,7 +3824,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) elcount = (i >> 1) + (i < 4 ? 4 : 2) + i; if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) { new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes, - PyArray_HANDLER(ret)->realloc); + PyArray_HANDLER(ret)->allocator); } else { new_data = NULL; @@ -3866,7 +3866,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) goto done; } new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize, - PyArray_HANDLER(ret)->realloc); + PyArray_HANDLER(ret)->allocator); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate array memory"); diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index c55a0778e399..32313867a257 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -394,7 +394,7 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) nbytes = dtype->elsize ? dtype->elsize : 1; } PyDataMem_UserFREE(PyArray_DATA(self), nbytes, - PyArray_HANDLER(self)->free); + PyArray_HANDLER(self)->allocator); } if (PyArray_BASE(self)) { if ((PyArray_FLAGS(self) & NPY_ARRAY_WRITEBACKIFCOPY) || diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 1d48916098a9..e4fb952c2067 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -970,7 +970,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, size = it->size; if (needcopy) { - buffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->alloc); + buffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->allocator); if (buffer == NULL) { ret = -1; goto fail; @@ -1054,7 +1054,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - PyDataMem_UserFREE(buffer, N * elsize, PyArray_HANDLER(op)->free); + PyDataMem_UserFREE(buffer, N * elsize, PyArray_HANDLER(op)->allocator); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -1116,7 +1116,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->alloc); + valbuffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->allocator); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1125,7 +1125,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, if (needidxbuffer) { idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), - PyArray_HANDLER(op)->alloc); + PyArray_HANDLER(op)->allocator); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1214,8 +1214,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - PyDataMem_UserFREE(valbuffer, N * elsize, PyArray_HANDLER(op)->free); - PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), PyArray_HANDLER(op)->free); + PyDataMem_UserFREE(valbuffer, N * elsize, PyArray_HANDLER(op)->allocator); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), PyArray_HANDLER(op)->allocator); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 7cb23d086860..a090f746093f 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2054,7 +2054,7 @@ array_setstate(PyArrayObject *self, PyObject *args) * line 820 */ PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, - PyArray_HANDLER(self)->free); + PyArray_HANDLER(self)->allocator); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } Py_XDECREF(PyArray_BASE(self)); @@ -2098,7 +2098,7 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); Py_RETURN_NONE; } - fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->alloc); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { Py_DECREF(rawdata); return PyErr_NoMemory(); @@ -2148,7 +2148,7 @@ array_setstate(PyArrayObject *self, PyObject *args) if (num == 0 || elsize == 0) { Py_RETURN_NONE; } - fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->alloc); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { return PyErr_NoMemory(); } diff --git a/numpy/core/src/multiarray/shape.c b/numpy/core/src/multiarray/shape.c index a67063987c89..c3612ece8d22 100644 --- a/numpy/core/src/multiarray/shape.c +++ b/numpy/core/src/multiarray/shape.c @@ -122,7 +122,7 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, /* Reallocate space if needed - allocating 0 is forbidden */ new_data = PyDataMem_UserRENEW(PyArray_DATA(self), newnbytes == 0 ? elsize : newnbytes, - PyArray_HANDLER(self)->realloc); + PyArray_HANDLER(self)->allocator); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory for array"); diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 764dc0900d89..52c0085b999b 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -35,9 +35,15 @@ def get_module(tmp_path): prologue = ''' #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include + typedef struct { + void *(*malloc)(size_t); + void *(*calloc)(size_t, size_t); + void *(*realloc)(void *, size_t); + void (*free)(void *); + } Allocator; NPY_NO_EXPORT void * - shift_alloc(size_t sz) { - char *real = (char *)malloc(sz + 64); + shift_alloc(Allocator *ctx, size_t sz) { + char *real = (char *)ctx->malloc(sz + 64); if (real == NULL) { return NULL; } @@ -45,8 +51,8 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void * - shift_zero(size_t sz, size_t cnt) { - char *real = (char *)calloc(sz + 64, cnt); + shift_zero(Allocator *ctx, size_t sz, size_t cnt) { + char *real = (char *)ctx->calloc(sz + 64, cnt); if (real == NULL) { return NULL; } @@ -55,7 +61,7 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void - shift_free(void * p, npy_uintp sz) { + shift_free(Allocator *ctx, void * p, npy_uintp sz) { if (p == NULL) { return ; } @@ -64,8 +70,8 @@ def get_module(tmp_path): fprintf(stdout, "uh-oh, unmatched shift_free, " "no appropriate prefix\\n"); /* Make C runtime crash by calling free on the wrong address */ - free((char *)p + 10); - /* free(real); */ + ctx->free((char *)p + 10); + /* ctx->free(real); */ } else { npy_uintp i = (npy_uintp)atoi(real +20); @@ -73,25 +79,25 @@ def get_module(tmp_path): fprintf(stderr, "uh-oh, unmatched shift_free" "(ptr, %ld) but allocated %ld\\n", sz, i); /* This happens in some places, only print */ - free(real); + ctx->free(real); } else { - free(real); + ctx->free(real); } } } NPY_NO_EXPORT void * - shift_realloc(void * p, npy_uintp sz) { + shift_realloc(Allocator *ctx, void * p, npy_uintp sz) { if (p != NULL) { char *real = (char *)p - 64; if (strncmp(real, "originally allocated", 20) != 0) { fprintf(stdout, "uh-oh, unmatched shift_realloc\\n"); return realloc(p, sz); } - return (void *)((char *)realloc(real, sz + 64) + 64); + return (void *)((char *)ctx->realloc(real, sz + 64) + 64); } else { - char *real = (char *)realloc(p, sz + 64); + char *real = (char *)ctx->realloc(p, sz + 64); if (real == NULL) { return NULL; } @@ -100,12 +106,21 @@ def get_module(tmp_path): return (void *)(real + 64); } } + static Allocator new_handler_ctx = { + malloc, + calloc, + realloc, + free + }; static PyDataMem_Handler new_handler = { "secret_data_allocator", - shift_alloc, /* alloc */ - shift_zero, /* zeroed_alloc */ - shift_free, /* free */ - shift_realloc /* realloc */ + { + &new_handler_ctx, + shift_alloc, /* malloc */ + shift_zero, /* calloc */ + shift_realloc, /* realloc */ + shift_free /* free */ + } }; ''' more_init = "import_array();" From 18bea05dfe63cf7b117a47bcd971155d8dc0fc89 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 30 Jun 2021 16:58:04 +0300 Subject: [PATCH 16/76] Fix incompatible pointer warnings --- numpy/core/src/multiarray/alloc.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 3d5ef9d96fc2..3fe0d3f38f5f 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -329,11 +329,11 @@ PyDataMem_RENEW(void *ptr, size_t size) PyDataMem_Handler default_handler = { "default_allocator", { - NULL, /* ctx */ - npy_alloc_cache, /* malloc */ - npy_alloc_cache_zero, /* calloc */ - PyDataMem_RENEW, /* realloc */ - npy_free_cache /* free */ + NULL, /* ctx */ + (void *(*)(void *, size_t)) npy_alloc_cache, /* malloc */ + (void *(*)(void *, size_t, size_t)) npy_alloc_cache_zero, /* calloc */ + (void *(*)(void *, void *, size_t)) PyDataMem_RENEW, /* realloc */ + (void (*)(void *, void *, size_t)) npy_free_cache /* free */ } }; @@ -348,7 +348,7 @@ PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) { void *result; - if (allocator.malloc == npy_alloc_cache) { + if ((void *) allocator.malloc == (void *) npy_alloc_cache) { // All the logic below is conditionally handled by npy_alloc_cache return npy_alloc_cache(size); } @@ -371,7 +371,7 @@ NPY_NO_EXPORT void * PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) { void *result; - if (allocator.calloc == npy_alloc_cache_zero) { + if ((void *) allocator.calloc == (void *) npy_alloc_cache_zero) { // All the logic below is conditionally handled by npy_alloc_cache_zero return npy_alloc_cache_zero(nmemb, size); } @@ -393,7 +393,7 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator NPY_NO_EXPORT void PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (allocator.free == npy_free_cache) { + if ((void *) allocator.free == (void *) npy_free_cache) { // All the logic below is conditionally handled by npy_free_cache npy_free_cache(ptr, size); return; @@ -414,7 +414,7 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) NPY_NO_EXPORT void * PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (allocator.realloc == PyDataMem_RENEW) { + if ((void *) allocator.realloc == (void *) PyDataMem_RENEW) { // All the logic below is conditionally handled by PyDataMem_RENEW return PyDataMem_RENEW(ptr, size); } From 43680230898100fceea7c983c039655c4dc749dd Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Thu, 1 Jul 2021 11:16:03 +0300 Subject: [PATCH 17/76] Note PyDataMemAllocator and PyMemAllocatorEx differentiation Co-authored-by: Matti Picus --- doc/source/reference/c-api/data_memory.rst | 2 +- numpy/core/include/numpy/ndarraytypes.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index ad143bd7d0c0..e475e3ddb0e6 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -68,6 +68,7 @@ reallocate or free the data memory of the instance. .. code-block:: c + /* The declaration of free differs from PyMemAllocatorEx */ typedef struct { void *ctx; void* (*malloc) (void *ctx, size_t size); @@ -119,4 +120,3 @@ For an example of setting up and using the PyDataMem_Handler, see the test in operations that might cause new allocation events (such as the creation/destruction numpy objects, or creating/destroying Python objects which might cause a gc) - diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index 80313685a81c..a6e4fbe1a186 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -667,6 +667,7 @@ typedef struct _arr_descr { /* * Memory handler structure for array data. */ +/* The declaration of free differs from PyMemAllocatorEx */ typedef struct { void *ctx; void* (*malloc) (void *ctx, size_t size); From d2433137e83de91e8e4e82c0446de9601f18d718 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Thu, 1 Jul 2021 12:02:03 +0300 Subject: [PATCH 18/76] Redefine default allocator handling --- numpy/core/src/multiarray/alloc.c | 26 ++++++++++++++++---------- tools/lint_diff.ini | 1 - 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 3fe0d3f38f5f..8c62a249a971 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -329,11 +329,11 @@ PyDataMem_RENEW(void *ptr, size_t size) PyDataMem_Handler default_handler = { "default_allocator", { - NULL, /* ctx */ - (void *(*)(void *, size_t)) npy_alloc_cache, /* malloc */ - (void *(*)(void *, size_t, size_t)) npy_alloc_cache_zero, /* calloc */ - (void *(*)(void *, void *, size_t)) PyDataMem_RENEW, /* realloc */ - (void (*)(void *, void *, size_t)) npy_free_cache /* free */ + NULL, /* ctx */ + NULL, /* (npy_alloc_cache) malloc */ + NULL, /* (npy_alloc_cache_zero) calloc */ + NULL, /* (PyDataMem_RENEW) realloc */ + NULL /* (npy_free_cache) free */ } }; @@ -341,14 +341,20 @@ PyDataMem_Handler *current_handler = &default_handler; int uo_index=0; /* user_override index */ -/* Wrappers for user-assigned PyDataMem_Handlers */ +/* + * Wrappers for user-assigned PyDataMem_Handlers + * + * The default data mem allocator routines do not make use of a ctx + * and are specially handled since they already integrate + * eventhook and tracemalloc logic. + */ NPY_NO_EXPORT void * PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) { void *result; - if ((void *) allocator.malloc == (void *) npy_alloc_cache) { + if (!allocator.malloc) { // All the logic below is conditionally handled by npy_alloc_cache return npy_alloc_cache(size); } @@ -371,7 +377,7 @@ NPY_NO_EXPORT void * PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) { void *result; - if ((void *) allocator.calloc == (void *) npy_alloc_cache_zero) { + if (!allocator.calloc) { // All the logic below is conditionally handled by npy_alloc_cache_zero return npy_alloc_cache_zero(nmemb, size); } @@ -393,7 +399,7 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator NPY_NO_EXPORT void PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) { - if ((void *) allocator.free == (void *) npy_free_cache) { + if (!allocator.free) { // All the logic below is conditionally handled by npy_free_cache npy_free_cache(ptr, size); return; @@ -414,7 +420,7 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) NPY_NO_EXPORT void * PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) { - if ((void *) allocator.realloc == (void *) PyDataMem_RENEW) { + if (!allocator.realloc) { // All the logic below is conditionally handled by PyDataMem_RENEW return PyDataMem_RENEW(ptr, size); } diff --git a/tools/lint_diff.ini b/tools/lint_diff.ini index a8f7f21bab8f..9e31050b78a4 100644 --- a/tools/lint_diff.ini +++ b/tools/lint_diff.ini @@ -3,4 +3,3 @@ max_line_length = 79 statistics = True ignore = E121,E122,E123,E125,E126,E127,E128,E226,E241,E251,E265,E266,E302,E402,E704,E712,E721,E731,E741,W291,W293,W391,W503,W504 exclude = numpy/__config__.py,numpy/typing/tests/data ->>>>>>> fixes from linter From c9b6854aa6bf60c06fcca5365d0227c5bf7649bf Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 5 Jul 2021 12:30:14 +0300 Subject: [PATCH 19/76] Always allocate new arrays using the current_handler --- numpy/core/src/multiarray/arraytypes.c.src | 10 +++++----- numpy/core/src/multiarray/ctors.c | 6 +++--- numpy/core/src/multiarray/item_selection.c | 12 ++++++------ numpy/core/src/multiarray/methods.c | 6 ++++++ 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 9529f2af745d..ac12d74d5f64 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3117,7 +3117,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (swap || new->alignment > 1) { if (swap || !npy_is_aligned(nip1, new->alignment)) { /* create buffer and copy */ - nip1 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->allocator); + nip1 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); if (nip1 == NULL) { goto finish; } @@ -3127,11 +3127,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } if (swap || !npy_is_aligned(nip2, new->alignment)) { /* create buffer and copy */ - nip2 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->allocator); + nip2 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); if (nip2 == NULL) { if (nip1 != ip1 + offset) { PyDataMem_UserFREE(nip1, new->elsize, - PyArray_HANDLER(ap)->allocator); + current_handler->allocator); } goto finish; } @@ -3143,10 +3143,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, dummy); if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { - PyDataMem_UserFREE(nip1, new->elsize, PyArray_HANDLER(ap)->allocator); + PyDataMem_UserFREE(nip1, new->elsize, current_handler->allocator); } if (nip2 != ip2 + offset) { - PyDataMem_UserFREE(nip2, new->elsize, PyArray_HANDLER(ap)->allocator); + PyDataMem_UserFREE(nip2, new->elsize, current_handler->allocator); } } if (res != 0) { diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 5df1aa1146f3..796b0d3b3c30 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -806,7 +806,7 @@ PyArray_NewFromDescr_int( if (data == NULL) { - /* Store the functions in case the global hander is modified */ + /* Store the functions in case the global handler is modified */ fa->mem_handler = current_handler; /* * Allocate something even for zero-space arrays @@ -837,8 +837,8 @@ PyArray_NewFromDescr_int( fa->flags |= NPY_ARRAY_OWNDATA; } else { - /* The handlers should never be called in this case, but just in case */ - fa->mem_handler = &default_handler; + /* The handlers should never be called in this case */ + fa->mem_handler = NULL; /* * If data is passed in, this object won't own it by default. * Caller must arrange for this to be reset if truly desired diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index e4fb952c2067..1c9ff60ee793 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -970,7 +970,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, size = it->size; if (needcopy) { - buffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->allocator); + buffer = PyDataMem_UserNEW(N * elsize, current_handler->allocator); if (buffer == NULL) { ret = -1; goto fail; @@ -1054,7 +1054,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - PyDataMem_UserFREE(buffer, N * elsize, PyArray_HANDLER(op)->allocator); + PyDataMem_UserFREE(buffer, N * elsize, current_handler->allocator); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -1116,7 +1116,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->allocator); + valbuffer = PyDataMem_UserNEW(N * elsize, current_handler->allocator); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1125,7 +1125,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, if (needidxbuffer) { idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), - PyArray_HANDLER(op)->allocator); + current_handler->allocator); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1214,8 +1214,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - PyDataMem_UserFREE(valbuffer, N * elsize, PyArray_HANDLER(op)->allocator); - PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), PyArray_HANDLER(op)->allocator); + PyDataMem_UserFREE(valbuffer, N * elsize, current_handler->allocator); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), current_handler->allocator); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index a090f746093f..87fb82428225 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2098,6 +2098,8 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); Py_RETURN_NONE; } + /* Store the functions in case the global handler is modified */ + fa->mem_handler = current_handler; fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { Py_DECREF(rawdata); @@ -2135,6 +2137,8 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); } else { + /* The handlers should never be called in this case */ + fa->mem_handler = NULL; fa->data = datastr; if (PyArray_SetBaseObject(self, rawdata) < 0) { Py_DECREF(rawdata); @@ -2148,6 +2152,8 @@ array_setstate(PyArrayObject *self, PyObject *args) if (num == 0 || elsize == 0) { Py_RETURN_NONE; } + /* Store the functions in case the global handler is modified */ + fa->mem_handler = current_handler; fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { return PyErr_NoMemory(); From a8fd37818014207227361534ff27a7a2f456d584 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 5 Jul 2021 13:42:39 +0300 Subject: [PATCH 20/76] Search for the mem_handler name of the data owner --- doc/source/reference/c-api/data_memory.rst | 6 ++++-- numpy/core/src/multiarray/alloc.c | 23 +++++++++++++++++++--- numpy/core/tests/test_mem_policy.py | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index e475e3ddb0e6..2449c75ad37b 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -64,7 +64,7 @@ reallocate or free the data memory of the instance. PyDataMemAllocator allocator; } PyDataMem_Handler; - where the allocator structure is: + where the allocator structure is .. code-block:: c @@ -88,7 +88,9 @@ reallocate or free the data memory of the instance. .. c:function:: const char * PyDataMem_GetHandlerName(PyArrayObject *obj) Return the const char name of the `PyDataMem_Handler` used by the - ``PyArrayObject``. If ``NULL``, return the name of the current global policy + ``PyArrayObject`` or its base. If neither the ``PyArrayObject`` owns its own + data nor its base is a ``PyArrayObject`` which owns its own data return an + empty string. If ``NULL``, return the name of the current global policy that will be used to allocate data for the next ``PyArrayObject``. For an example of setting up and using the PyDataMem_Handler, see the test in diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 8c62a249a971..dec0698c7d5c 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -467,8 +467,10 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) /*NUMPY_API * Return the const char name of the PyDataMem_Handler used by the - * PyArrayObject. If NULL, return the name of the current global policy that - * will be used to allocate data for the next PyArrayObject + * PyArrayObject or its base. If neither the PyArrayObject owns its own data + * nor its base is a PyArrayObject which owns its own data return an empty string. + * If NULL, return the name of the current global policy that + * will be used to allocate data for the next PyArrayObject. */ NPY_NO_EXPORT const char * PyDataMem_GetHandlerName(PyArrayObject *obj) @@ -476,7 +478,19 @@ PyDataMem_GetHandlerName(PyArrayObject *obj) if (obj == NULL) { return current_handler->name; } - return PyArray_HANDLER(obj)->name; + PyDataMem_Handler *handler; + handler = PyArray_HANDLER(obj); + if (handler != NULL) { + return handler->name; + } + PyObject *base = PyArray_BASE(obj); + if (base != NULL && PyArray_Check(base)) { + handler = PyArray_HANDLER((PyArrayObject *) base); + if (handler != NULL) { + return handler->name; + } + } + return ""; } NPY_NO_EXPORT PyObject * @@ -494,5 +508,8 @@ get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args) if (name == NULL) { return NULL; } + else if (strlen(name) == 0) { + Py_RETURN_NONE; + } return PyUnicode_FromString(name); } diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 52c0085b999b..9285ffeab6db 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -152,7 +152,7 @@ def test_new_policy(get_module): a = np.arange(10) orig_policy = get_module.test_prefix(a) assert get_module.set_new_policy() == orig_policy - b = np.arange(10) + b = np.arange(10).reshape((2, 5)) assert get_module.test_prefix(b) == 'secret_data_allocator' # test array manipulation. This is slow From a17565b5eda8a2aa4e13bc8971db2237f3135f2b Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 5 Jul 2021 16:59:24 +0300 Subject: [PATCH 21/76] Sub-comparisons don't need a local mem_handler --- numpy/core/src/multiarray/arraytypes.c.src | 14 +++++++++++--- numpy/core/src/multiarray/item_selection.c | 2 ++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index ac12d74d5f64..572413d4572f 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3109,14 +3109,16 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } /* Set the fields needed by compare or copyswap */ dummy_struct.descr = new; - dummy_struct.mem_handler = PyArray_HANDLER(ap); swap = PyArray_ISBYTESWAPPED(dummy); nip1 = ip1 + offset; nip2 = ip2 + offset; if (swap || new->alignment > 1) { if (swap || !npy_is_aligned(nip1, new->alignment)) { - /* create buffer and copy */ + /* + * create temporary buffer and copy, + * always use the current handler for internal allocations + */ nip1 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); if (nip1 == NULL) { goto finish; @@ -3126,10 +3128,14 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) new->f->copyswap(nip1, NULL, swap, dummy); } if (swap || !npy_is_aligned(nip2, new->alignment)) { - /* create buffer and copy */ + /* + * create temporary buffer and copy, + * always use the current handler for internal allocations + */ nip2 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); if (nip2 == NULL) { if (nip1 != ip1 + offset) { + /* destroy temporary buffer */ PyDataMem_UserFREE(nip1, new->elsize, current_handler->allocator); } @@ -3143,9 +3149,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, dummy); if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { + /* destroy temporary buffer */ PyDataMem_UserFREE(nip1, new->elsize, current_handler->allocator); } if (nip2 != ip2 + offset) { + /* destroy temporary buffer */ PyDataMem_UserFREE(nip2, new->elsize, current_handler->allocator); } } diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 1c9ff60ee793..0548b08cc31d 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -1054,6 +1054,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); + /* cleanup internal buffer */ PyDataMem_UserFREE(buffer, N * elsize, current_handler->allocator); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ @@ -1214,6 +1215,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); + /* cleanup internal buffers */ PyDataMem_UserFREE(valbuffer, N * elsize, current_handler->allocator); PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), current_handler->allocator); if (ret < 0) { From 2ec591221eca086fc03da47a788e7ac0efd002c5 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 14 Jul 2021 15:02:45 +0300 Subject: [PATCH 22/76] Make the default_handler a valid PyDataMem_Handler --- numpy/core/src/multiarray/alloc.c | 86 ++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index dec0698c7d5c..1a937507c6ad 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -325,15 +325,64 @@ PyDataMem_RENEW(void *ptr, size_t size) return result; } +// The default data mem allocator malloc routine does not make use of a ctx. +// It should be called only through PyDataMem_UserNEW +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void * +default_malloc(void *NPY_UNUSED(ctx), size_t size) +{ + return _npy_alloc_cache(size, 1, NBUCKETS, datacache, &malloc); +} + +// The default data mem allocator calloc routine does not make use of a ctx. +// It should be called only through PyDataMem_UserNEW_ZEROED +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void * +default_calloc(void *NPY_UNUSED(ctx), size_t nelem, size_t elsize) +{ + void * p; + size_t sz = nelem * elsize; + NPY_BEGIN_THREADS_DEF; + if (sz < NBUCKETS) { + p = _npy_alloc_cache(sz, 1, NBUCKETS, datacache, &malloc); + if (p) { + memset(p, 0, sz); + } + return p; + } + NPY_BEGIN_THREADS; + p = calloc(nelem, elsize); + NPY_END_THREADS; + return p; +} + +// The default data mem allocator realloc routine does not make use of a ctx. +// It should be called only through PyDataMem_UserRENEW +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void * +default_realloc(void *NPY_UNUSED(ctx), void *ptr, size_t new_size) +{ + return realloc(ptr, new_size); +} + +// The default data mem allocator free routine does not make use of a ctx. +// It should be called only through PyDataMem_UserFREE +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void +default_free(void *NPY_UNUSED(ctx), void *ptr, size_t size) +{ + _npy_free_cache(ptr, size, NBUCKETS, datacache, &free); +} + /* Memory handler global default */ PyDataMem_Handler default_handler = { "default_allocator", { - NULL, /* ctx */ - NULL, /* (npy_alloc_cache) malloc */ - NULL, /* (npy_alloc_cache_zero) calloc */ - NULL, /* (PyDataMem_RENEW) realloc */ - NULL /* (npy_free_cache) free */ + NULL, /* ctx */ + default_malloc, /* malloc */ + default_calloc, /* calloc */ + default_realloc, /* realloc */ + default_free /* free */ } }; @@ -341,23 +390,13 @@ PyDataMem_Handler *current_handler = &default_handler; int uo_index=0; /* user_override index */ -/* - * Wrappers for user-assigned PyDataMem_Handlers - * - * The default data mem allocator routines do not make use of a ctx - * and are specially handled since they already integrate - * eventhook and tracemalloc logic. - */ +/* Wrappers for the default or any user-assigned PyDataMem_Handler */ NPY_NO_EXPORT void * PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) { void *result; - if (!allocator.malloc) { - // All the logic below is conditionally handled by npy_alloc_cache - return npy_alloc_cache(size); - } assert(size != 0); result = allocator.malloc(allocator.ctx, size); if (_PyDataMem_eventhook != NULL) { @@ -377,11 +416,6 @@ NPY_NO_EXPORT void * PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) { void *result; - if (!allocator.calloc) { - // All the logic below is conditionally handled by npy_alloc_cache_zero - return npy_alloc_cache_zero(nmemb, size); - } - result = allocator.calloc(allocator.ctx, nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF @@ -399,11 +433,6 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator NPY_NO_EXPORT void PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (!allocator.free) { - // All the logic below is conditionally handled by npy_free_cache - npy_free_cache(ptr, size); - return; - } PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); allocator.free(allocator.ctx, ptr, size); if (_PyDataMem_eventhook != NULL) { @@ -420,11 +449,6 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) NPY_NO_EXPORT void * PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (!allocator.realloc) { - // All the logic below is conditionally handled by PyDataMem_RENEW - return PyDataMem_RENEW(ptr, size); - } - void *result; assert(size != 0); From 227c4b8fdbdd7b2e6c1813ad76f0c91c4c11c4f8 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 14 Jul 2021 15:03:24 +0300 Subject: [PATCH 23/76] Fix PyDataMem_SetHandler description (NEP discussion) --- numpy/core/src/multiarray/alloc.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 1a937507c6ad..7f81effc56bc 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -471,10 +471,9 @@ PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) /*NUMPY_API * Sets a new allocation policy. If the input value is NULL, will reset - * the policy to the default. Returns the previous policy, NULL if the - * previous policy was the default. We wrap the user-provided functions - * so they will still call the python and numpy memory management callback - * hooks. + * the policy to the default. Returns the previous policy. We wrap + * the user-provided functions so they will still call the python + * and numpy memory management callback hooks. */ NPY_NO_EXPORT const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) From fb8135d44961fb80322da06d49d466723e22f728 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 14 Jul 2021 19:50:58 +0300 Subject: [PATCH 24/76] Pass the allocators by reference --- numpy/core/src/multiarray/alloc.c | 16 ++++++++-------- numpy/core/src/multiarray/alloc.h | 8 ++++---- numpy/core/src/multiarray/arrayobject.c | 2 +- numpy/core/src/multiarray/arraytypes.c.src | 10 +++++----- numpy/core/src/multiarray/ctors.c | 14 +++++++------- numpy/core/src/multiarray/getset.c | 2 +- numpy/core/src/multiarray/item_selection.c | 12 ++++++------ numpy/core/src/multiarray/methods.c | 6 +++--- numpy/core/src/multiarray/shape.c | 2 +- 9 files changed, 36 insertions(+), 36 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 7f81effc56bc..461fe959ffec 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -393,12 +393,12 @@ int uo_index=0; /* user_override index */ /* Wrappers for the default or any user-assigned PyDataMem_Handler */ NPY_NO_EXPORT void * -PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) +PyDataMem_UserNEW(size_t size, const PyDataMemAllocator *allocator) { void *result; assert(size != 0); - result = allocator.malloc(allocator.ctx, size); + result = allocator->malloc(allocator->ctx, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -413,10 +413,10 @@ PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) } NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, const PyDataMemAllocator *allocator) { void *result; - result = allocator.calloc(allocator.ctx, nmemb, size); + result = allocator->calloc(allocator->ctx, nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -431,10 +431,10 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator } NPY_NO_EXPORT void -PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) +PyDataMem_UserFREE(void *ptr, size_t size, const PyDataMemAllocator *allocator) { PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); - allocator.free(allocator.ctx, ptr, size); + allocator->free(allocator->ctx, ptr, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -447,12 +447,12 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) } NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) +PyDataMem_UserRENEW(void *ptr, size_t size, const PyDataMemAllocator *allocator) { void *result; assert(size != 0); - result = allocator.realloc(allocator.ctx, ptr, size); + result = allocator->realloc(allocator->ctx, ptr, size); if (result != ptr) { PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); } diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index 12a859b44e98..cac40d0d9f82 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -10,16 +10,16 @@ NPY_NO_EXPORT PyObject * _set_madvise_hugepage(PyObject *NPY_UNUSED(self), PyObject *enabled_obj); NPY_NO_EXPORT void * -PyDataMem_UserNEW(npy_uintp sz, PyDataMemAllocator allocator); +PyDataMem_UserNEW(npy_uintp sz, const PyDataMemAllocator *allocator); NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator); +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, const PyDataMemAllocator *allocator); NPY_NO_EXPORT void -PyDataMem_UserFREE(void * p, npy_uintp sd, PyDataMemAllocator allocator); +PyDataMem_UserFREE(void * p, npy_uintp sd, const PyDataMemAllocator *allocator); NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator); +PyDataMem_UserRENEW(void *ptr, size_t size, const PyDataMemAllocator *allocator); NPY_NO_EXPORT void * npy_alloc_cache_dim(npy_uintp sz); diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index 926813cdbf75..37fd54611a6a 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -501,7 +501,7 @@ array_dealloc(PyArrayObject *self) if (nbytes == 0) { nbytes = fa->descr->elsize ? fa->descr->elsize : 1; } - PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler->allocator); + PyDataMem_UserFREE(fa->data, nbytes, &fa->mem_handler->allocator); } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 572413d4572f..d758a3caea9d 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3119,7 +3119,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) * create temporary buffer and copy, * always use the current handler for internal allocations */ - nip1 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); + nip1 = PyDataMem_UserNEW(new->elsize, ¤t_handler->allocator); if (nip1 == NULL) { goto finish; } @@ -3132,12 +3132,12 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) * create temporary buffer and copy, * always use the current handler for internal allocations */ - nip2 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); + nip2 = PyDataMem_UserNEW(new->elsize, ¤t_handler->allocator); if (nip2 == NULL) { if (nip1 != ip1 + offset) { /* destroy temporary buffer */ PyDataMem_UserFREE(nip1, new->elsize, - current_handler->allocator); + ¤t_handler->allocator); } goto finish; } @@ -3150,11 +3150,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { /* destroy temporary buffer */ - PyDataMem_UserFREE(nip1, new->elsize, current_handler->allocator); + PyDataMem_UserFREE(nip1, new->elsize, ¤t_handler->allocator); } if (nip2 != ip2 + offset) { /* destroy temporary buffer */ - PyDataMem_UserFREE(nip2, new->elsize, current_handler->allocator); + PyDataMem_UserFREE(nip2, new->elsize, ¤t_handler->allocator); } } if (res != 0) { diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 796b0d3b3c30..59e5fe7123c5 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -824,10 +824,10 @@ PyArray_NewFromDescr_int( */ if (zeroed || PyDataType_FLAGCHK(descr, NPY_NEEDS_INIT)) { data = PyDataMem_UserNEW_ZEROED(nbytes, 1, - fa->mem_handler->allocator); + &fa->mem_handler->allocator); } else { - data = PyDataMem_UserNEW(nbytes, fa->mem_handler->allocator); + data = PyDataMem_UserNEW(nbytes, &fa->mem_handler->allocator); } if (data == NULL) { raise_memory_error(fa->nd, fa->dimensions, descr); @@ -3410,7 +3410,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre if (num < 0 && thisbuf == size) { totalbytes += bytes; tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes, - PyArray_HANDLER(r)->allocator); + &PyArray_HANDLER(r)->allocator); if (tmp == NULL) { err = 1; break; @@ -3433,7 +3433,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre if (nsize != 0) { tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize, - PyArray_HANDLER(r)->allocator); + &PyArray_HANDLER(r)->allocator); if (tmp == NULL) { err = 1; } @@ -3539,7 +3539,7 @@ PyArray_FromFile(FILE *fp, PyArray_Descr *dtype, npy_intp num, char *sep) char *tmp; if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize, - PyArray_HANDLER(ret)->allocator)) == NULL) { + &PyArray_HANDLER(ret)->allocator)) == NULL) { Py_DECREF(dtype); Py_DECREF(ret); return PyErr_NoMemory(); @@ -3824,7 +3824,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) elcount = (i >> 1) + (i < 4 ? 4 : 2) + i; if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) { new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes, - PyArray_HANDLER(ret)->allocator); + &PyArray_HANDLER(ret)->allocator); } else { new_data = NULL; @@ -3866,7 +3866,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) goto done; } new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize, - PyArray_HANDLER(ret)->allocator); + &PyArray_HANDLER(ret)->allocator); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate array memory"); diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index 32313867a257..3d28df10b79a 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -394,7 +394,7 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) nbytes = dtype->elsize ? dtype->elsize : 1; } PyDataMem_UserFREE(PyArray_DATA(self), nbytes, - PyArray_HANDLER(self)->allocator); + &PyArray_HANDLER(self)->allocator); } if (PyArray_BASE(self)) { if ((PyArray_FLAGS(self) & NPY_ARRAY_WRITEBACKIFCOPY) || diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 0548b08cc31d..f254e3fb0ed8 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -970,7 +970,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, size = it->size; if (needcopy) { - buffer = PyDataMem_UserNEW(N * elsize, current_handler->allocator); + buffer = PyDataMem_UserNEW(N * elsize, ¤t_handler->allocator); if (buffer == NULL) { ret = -1; goto fail; @@ -1055,7 +1055,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); /* cleanup internal buffer */ - PyDataMem_UserFREE(buffer, N * elsize, current_handler->allocator); + PyDataMem_UserFREE(buffer, N * elsize, ¤t_handler->allocator); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -1117,7 +1117,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = PyDataMem_UserNEW(N * elsize, current_handler->allocator); + valbuffer = PyDataMem_UserNEW(N * elsize, ¤t_handler->allocator); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1126,7 +1126,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, if (needidxbuffer) { idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), - current_handler->allocator); + ¤t_handler->allocator); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1216,8 +1216,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); /* cleanup internal buffers */ - PyDataMem_UserFREE(valbuffer, N * elsize, current_handler->allocator); - PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), current_handler->allocator); + PyDataMem_UserFREE(valbuffer, N * elsize, ¤t_handler->allocator); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), ¤t_handler->allocator); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 87fb82428225..1a8446e49340 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2054,7 +2054,7 @@ array_setstate(PyArrayObject *self, PyObject *args) * line 820 */ PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, - PyArray_HANDLER(self)->allocator); + &PyArray_HANDLER(self)->allocator); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } Py_XDECREF(PyArray_BASE(self)); @@ -2100,7 +2100,7 @@ array_setstate(PyArrayObject *self, PyObject *args) } /* Store the functions in case the global handler is modified */ fa->mem_handler = current_handler; - fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); + fa->data = PyDataMem_UserNEW(num, &PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { Py_DECREF(rawdata); return PyErr_NoMemory(); @@ -2154,7 +2154,7 @@ array_setstate(PyArrayObject *self, PyObject *args) } /* Store the functions in case the global handler is modified */ fa->mem_handler = current_handler; - fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); + fa->data = PyDataMem_UserNEW(num, &PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { return PyErr_NoMemory(); } diff --git a/numpy/core/src/multiarray/shape.c b/numpy/core/src/multiarray/shape.c index c3612ece8d22..09cec841aed2 100644 --- a/numpy/core/src/multiarray/shape.c +++ b/numpy/core/src/multiarray/shape.c @@ -122,7 +122,7 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, /* Reallocate space if needed - allocating 0 is forbidden */ new_data = PyDataMem_UserRENEW(PyArray_DATA(self), newnbytes == 0 ? elsize : newnbytes, - PyArray_HANDLER(self)->allocator); + &PyArray_HANDLER(self)->allocator); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory for array"); From 7291484f981dde634524656e2c2888ca2d20be6d Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Sun, 8 Aug 2021 15:11:22 +0300 Subject: [PATCH 25/76] Implement allocator context-locality --- numpy/core/code_generators/numpy_api.py | 2 +- numpy/core/src/multiarray/alloc.c | 90 +++++++++++++------- numpy/core/src/multiarray/alloc.h | 2 +- numpy/core/src/multiarray/arraytypes.c.src | 17 ++-- numpy/core/src/multiarray/ctors.c | 5 +- numpy/core/src/multiarray/item_selection.c | 22 +++-- numpy/core/src/multiarray/methods.c | 11 ++- numpy/core/src/multiarray/multiarraymodule.c | 11 +++ numpy/core/tests/test_mem_policy.py | 2 +- 9 files changed, 112 insertions(+), 50 deletions(-) diff --git a/numpy/core/code_generators/numpy_api.py b/numpy/core/code_generators/numpy_api.py index 59ae7c592d25..3813c6ad7810 100644 --- a/numpy/core/code_generators/numpy_api.py +++ b/numpy/core/code_generators/numpy_api.py @@ -351,7 +351,7 @@ 'PyArray_SetWritebackIfCopyBase': (303,), # End 1.14 API 'PyDataMem_SetHandler': (304,), - 'PyDataMem_GetHandlerName': (305,), + 'PyDataMem_GetHandler': (305,), # End 1.21 API } diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 461fe959ffec..cfe32982d596 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -386,7 +386,7 @@ PyDataMem_Handler default_handler = { } }; -PyDataMem_Handler *current_handler = &default_handler; +PyObject *current_handler; int uo_index=0; /* user_override index */ @@ -470,50 +470,77 @@ PyDataMem_UserRENEW(void *ptr, size_t size, const PyDataMemAllocator *allocator) } /*NUMPY_API - * Sets a new allocation policy. If the input value is NULL, will reset - * the policy to the default. Returns the previous policy. We wrap - * the user-provided functions so they will still call the python - * and numpy memory management callback hooks. + * Set a new allocation policy. If the input value is NULL, will reset + * the policy to the default. Return the previous policy, or set an exception + * and return NULL if an error has occurred. We wrap the user-provided + * functions so they will still call the python and numpy + * memory management callback hooks. */ NPY_NO_EXPORT const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) { - const PyDataMem_Handler *old = current_handler; + PyObject *capsule; + PyObject *old_capsule; + PyDataMem_Handler *old_handler; + PyObject *token; + if (PyContextVar_Get(current_handler, NULL, &old_capsule)) { + return NULL; + } + old_handler = (PyDataMem_Handler *) PyCapsule_GetPointer(old_capsule, NULL); + if (old_handler == NULL) { + return NULL; + } if (handler) { - current_handler = handler; + capsule = PyCapsule_New(handler, NULL, NULL); + if (capsule == NULL) { + return NULL; + } } else { - current_handler = &default_handler; + capsule = PyCapsule_New(&default_handler, NULL, NULL); + if (capsule == NULL) { + return NULL; + } } - return old; + token = PyContextVar_Set(current_handler, capsule); + if (token == NULL) { + Py_DECREF(capsule); + return NULL; + } + Py_DECREF(old_capsule); + Py_DECREF(token); + return old_handler; } /*NUMPY_API - * Return the const char name of the PyDataMem_Handler used by the - * PyArrayObject or its base. If neither the PyArrayObject owns its own data - * nor its base is a PyArrayObject which owns its own data return an empty string. - * If NULL, return the name of the current global policy that - * will be used to allocate data for the next PyArrayObject. + * Return the PyDataMem_Handler used by the PyArrayObject. If NULL, return + * the current global policy that ill be used to allocate data + * for the next PyArrayObject. On failure, set an exception and return NULL. */ -NPY_NO_EXPORT const char * -PyDataMem_GetHandlerName(PyArrayObject *obj) +NPY_NO_EXPORT PyDataMem_Handler * +PyDataMem_GetHandler(PyArrayObject *obj) { + PyObject *base; + PyObject *capsule; + PyDataMem_Handler *handler; if (obj == NULL) { - return current_handler->name; + if (PyContextVar_Get(current_handler, NULL, &capsule)) { + return NULL; + } + return (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); } - PyDataMem_Handler *handler; + /* If there's a handler, the array owns its own datay */ handler = PyArray_HANDLER(obj); - if (handler != NULL) { - return handler->name; - } - PyObject *base = PyArray_BASE(obj); - if (base != NULL && PyArray_Check(base)) { - handler = PyArray_HANDLER((PyArrayObject *) base); - if (handler != NULL) { - return handler->name; + if (handler == NULL) { + /* + * If the base is an array which owns its own data, return its allocator. + */ + base = PyArray_BASE(obj); + if (base != NULL && PyArray_Check(base) && PyArray_CHKFLAGS(base, NPY_ARRAY_OWNDATA)) { + return PyArray_HANDLER(base); } } - return ""; + return handler; } NPY_NO_EXPORT PyObject * @@ -527,12 +554,9 @@ get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args) PyErr_SetString(PyExc_ValueError, "if supplied, argument must be an ndarray"); return NULL; } - const char * name = PyDataMem_GetHandlerName((PyArrayObject *)arr); - if (name == NULL) { + const PyDataMem_Handler * mem_handler = PyDataMem_GetHandler((PyArrayObject *)arr); + if (mem_handler == NULL) { return NULL; } - else if (strlen(name) == 0) { - Py_RETURN_NONE; - } - return PyUnicode_FromString(name); + return PyUnicode_FromString(mem_handler->name); } diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index cac40d0d9f82..9550ee2d5071 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -39,7 +39,7 @@ npy_free_cache_dim_array(PyArrayObject * arr) npy_free_cache_dim(PyArray_DIMS(arr), PyArray_NDIM(arr)); } -extern PyDataMem_Handler *current_handler; +extern PyObject *current_handler; /* PyContextVar/PyCapsule */ extern PyDataMem_Handler default_handler; NPY_NO_EXPORT PyObject * diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index d758a3caea9d..421205295933 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3082,6 +3082,7 @@ UNICODE_compare(npy_ucs4 *ip1, npy_ucs4 *ip2, static int VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) { + PyDataMem_Handler *mem_handler; PyArray_Descr *descr; PyObject *names, *key; PyObject *tup; @@ -3093,6 +3094,12 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (!PyArray_HASFIELDS(ap)) { return STRING_compare(ip1, ip2, ap); } + mem_handler = PyDataMem_GetHandler(NULL); + if (mem_handler == NULL) { + /* Better fallback to default than goto finish. */ + PyErr_Clear(); + mem_handler = &default_handler; + } descr = PyArray_DESCR(ap); /* * Compare on the first-field. If equal, then @@ -3119,7 +3126,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) * create temporary buffer and copy, * always use the current handler for internal allocations */ - nip1 = PyDataMem_UserNEW(new->elsize, ¤t_handler->allocator); + nip1 = PyDataMem_UserNEW(new->elsize, &mem_handler->allocator); if (nip1 == NULL) { goto finish; } @@ -3132,12 +3139,12 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) * create temporary buffer and copy, * always use the current handler for internal allocations */ - nip2 = PyDataMem_UserNEW(new->elsize, ¤t_handler->allocator); + nip2 = PyDataMem_UserNEW(new->elsize, &mem_handler->allocator); if (nip2 == NULL) { if (nip1 != ip1 + offset) { /* destroy temporary buffer */ PyDataMem_UserFREE(nip1, new->elsize, - ¤t_handler->allocator); + &mem_handler->allocator); } goto finish; } @@ -3150,11 +3157,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { /* destroy temporary buffer */ - PyDataMem_UserFREE(nip1, new->elsize, ¤t_handler->allocator); + PyDataMem_UserFREE(nip1, new->elsize, &mem_handler->allocator); } if (nip2 != ip2 + offset) { /* destroy temporary buffer */ - PyDataMem_UserFREE(nip2, new->elsize, ¤t_handler->allocator); + PyDataMem_UserFREE(nip2, new->elsize, &mem_handler->allocator); } } if (res != 0) { diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 59e5fe7123c5..047a91361779 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -807,7 +807,10 @@ PyArray_NewFromDescr_int( if (data == NULL) { /* Store the functions in case the global handler is modified */ - fa->mem_handler = current_handler; + fa->mem_handler = PyDataMem_GetHandler(NULL); + if (fa->mem_handler == NULL) { + goto fail; + } /* * Allocate something even for zero-space arrays * e.g. shape=(0,) -- otherwise buffer exposure diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index f254e3fb0ed8..cf5b73469e8b 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -951,6 +951,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, PyArray_CopySwapNFunc *copyswapn = PyArray_DESCR(op)->f->copyswapn; char *buffer = NULL; + PyDataMem_Handler *mem_handler; PyArrayIterObject *it; npy_intp size; @@ -963,6 +964,10 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, return 0; } + mem_handler = PyDataMem_GetHandler(NULL); + if (mem_handler == NULL) { + return -1; + } it = (PyArrayIterObject *)PyArray_IterAllButAxis((PyObject *)op, &axis); if (it == NULL) { return -1; @@ -970,7 +975,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, size = it->size; if (needcopy) { - buffer = PyDataMem_UserNEW(N * elsize, ¤t_handler->allocator); + buffer = PyDataMem_UserNEW(N * elsize, &mem_handler->allocator); if (buffer == NULL) { ret = -1; goto fail; @@ -1055,7 +1060,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); /* cleanup internal buffer */ - PyDataMem_UserFREE(buffer, N * elsize, ¤t_handler->allocator); + PyDataMem_UserFREE(buffer, N * elsize, &mem_handler->allocator); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -1082,6 +1087,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, char *valbuffer = NULL; npy_intp *idxbuffer = NULL; + PyDataMem_Handler *mem_handler; PyArrayObject *rop; npy_intp rstride; @@ -1092,6 +1098,10 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, NPY_BEGIN_THREADS_DEF; + mem_handler = PyDataMem_GetHandler(NULL); + if (mem_handler == NULL) { + return NULL; + } rop = (PyArrayObject *)PyArray_NewFromDescr( Py_TYPE(op), PyArray_DescrFromType(NPY_INTP), PyArray_NDIM(op), PyArray_DIMS(op), NULL, NULL, @@ -1117,7 +1127,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = PyDataMem_UserNEW(N * elsize, ¤t_handler->allocator); + valbuffer = PyDataMem_UserNEW(N * elsize, &mem_handler->allocator); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1126,7 +1136,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, if (needidxbuffer) { idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), - ¤t_handler->allocator); + &mem_handler->allocator); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1216,8 +1226,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); /* cleanup internal buffers */ - PyDataMem_UserFREE(valbuffer, N * elsize, ¤t_handler->allocator); - PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), ¤t_handler->allocator); + PyDataMem_UserFREE(valbuffer, N * elsize, &mem_handler->allocator); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), &mem_handler->allocator); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 1a8446e49340..5020e294fc06 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2099,7 +2099,11 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_RETURN_NONE; } /* Store the functions in case the global handler is modified */ - fa->mem_handler = current_handler; + fa->mem_handler = PyDataMem_GetHandler(NULL); + if (fa->mem_handler == NULL) { + Py_DECREF(rawdata); + return NULL; + } fa->data = PyDataMem_UserNEW(num, &PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { Py_DECREF(rawdata); @@ -2153,7 +2157,10 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_RETURN_NONE; } /* Store the functions in case the global handler is modified */ - fa->mem_handler = current_handler; + fa->mem_handler = PyDataMem_GetHandler(NULL); + if (fa->mem_handler == NULL) { + return NULL; + } fa->data = PyDataMem_UserNEW(num, &PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { return PyErr_NoMemory(); diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index 8db2666376c2..0bd0042c328c 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -4908,6 +4908,17 @@ PyMODINIT_FUNC PyInit__multiarray_umath(void) { if (initumath(m) != 0) { goto err; } + /* + * Initialize the context-local PyDataMem_Handler capsule. + */ + c_api = PyCapsule_New(&default_handler, NULL, NULL); + if (c_api == NULL) { + goto err; + } + current_handler = PyContextVar_New("current_allocator", c_api); + if (current_handler == NULL) { + goto err; + } return m; err: diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 9285ffeab6db..38e7597d9a88 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -18,7 +18,7 @@ def get_module(tmp_path): "must be called with a numpy scalar or ndarray"); } return PyUnicode_FromString( - PyDataMem_GetHandlerName((PyArrayObject*)args)); + PyDataMem_GetHandler((PyArrayObject*)args)->name); """ ), ("set_new_policy", "METH_NOARGS", From c7a9c225f7e1f7e6c06fe30c96e9b88ee0e70c22 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Sun, 8 Aug 2021 17:07:16 +0300 Subject: [PATCH 26/76] Fix documentation, make PyDataMem_GetHandler return const --- doc/source/reference/c-api/data_memory.rst | 16 +++++++--------- numpy/core/src/multiarray/alloc.c | 5 +++-- numpy/core/src/multiarray/arraytypes.c.src | 3 +-- numpy/core/src/multiarray/ctors.c | 2 +- numpy/core/src/multiarray/item_selection.c | 6 ++---- numpy/core/src/multiarray/methods.c | 4 ++-- 6 files changed, 16 insertions(+), 20 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index 2449c75ad37b..237922ab08ee 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -79,19 +79,17 @@ reallocate or free the data memory of the instance. .. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) - Sets a new allocation policy. If the input value is ``NULL``, will reset - the policy to the default. Returns the previous policy, ``NULL`` if the - previous policy was the default. We wrap the user-provided functions + Set a new allocation policy. If the input value is ``NULL``, will reset the + policy to the default. Return the previous policy, or set an exception and + return ``NULL`` if an error has occurred. We wrap the user-provided functions so they will still call the python and numpy memory management callback hooks. -.. c:function:: const char * PyDataMem_GetHandlerName(PyArrayObject *obj) +.. c:function:: const PyDataMem_Handler * PyDataMem_GetHandler(PyArrayObject *obj) - Return the const char name of the `PyDataMem_Handler` used by the - ``PyArrayObject`` or its base. If neither the ``PyArrayObject`` owns its own - data nor its base is a ``PyArrayObject`` which owns its own data return an - empty string. If ``NULL``, return the name of the current global policy - that will be used to allocate data for the next ``PyArrayObject``. + Return the `PyDataMem_Handler` used by the ``PyArrayObject``. If ``NULL``, + return the current global policy that ill be used to allocate data for the + next ``PyArrayObject``. On failure, set an exception and return ``NULL``. For an example of setting up and using the PyDataMem_Handler, see the test in :file:`numpy/core/tests/test_mem_policy.py` diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index cfe32982d596..11d1a42ccf0a 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -517,7 +517,7 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) * the current global policy that ill be used to allocate data * for the next PyArrayObject. On failure, set an exception and return NULL. */ -NPY_NO_EXPORT PyDataMem_Handler * +NPY_NO_EXPORT const PyDataMem_Handler * PyDataMem_GetHandler(PyArrayObject *obj) { PyObject *base; @@ -536,7 +536,8 @@ PyDataMem_GetHandler(PyArrayObject *obj) * If the base is an array which owns its own data, return its allocator. */ base = PyArray_BASE(obj); - if (base != NULL && PyArray_Check(base) && PyArray_CHKFLAGS(base, NPY_ARRAY_OWNDATA)) { + if (base != NULL && PyArray_Check(base) && + PyArray_CHKFLAGS((PyArrayObject *) base, NPY_ARRAY_OWNDATA)) { return PyArray_HANDLER(base); } } diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 421205295933..086e76aa6e5b 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3082,7 +3082,6 @@ UNICODE_compare(npy_ucs4 *ip1, npy_ucs4 *ip2, static int VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) { - PyDataMem_Handler *mem_handler; PyArray_Descr *descr; PyObject *names, *key; PyObject *tup; @@ -3094,7 +3093,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (!PyArray_HASFIELDS(ap)) { return STRING_compare(ip1, ip2, ap); } - mem_handler = PyDataMem_GetHandler(NULL); + const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(NULL); if (mem_handler == NULL) { /* Better fallback to default than goto finish. */ PyErr_Clear(); diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 047a91361779..c540abfbeae2 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -807,7 +807,7 @@ PyArray_NewFromDescr_int( if (data == NULL) { /* Store the functions in case the global handler is modified */ - fa->mem_handler = PyDataMem_GetHandler(NULL); + fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(NULL); if (fa->mem_handler == NULL) { goto fail; } diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index cf5b73469e8b..395f3e074d6a 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -951,7 +951,6 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, PyArray_CopySwapNFunc *copyswapn = PyArray_DESCR(op)->f->copyswapn; char *buffer = NULL; - PyDataMem_Handler *mem_handler; PyArrayIterObject *it; npy_intp size; @@ -964,7 +963,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, return 0; } - mem_handler = PyDataMem_GetHandler(NULL); + const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(NULL); if (mem_handler == NULL) { return -1; } @@ -1087,7 +1086,6 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, char *valbuffer = NULL; npy_intp *idxbuffer = NULL; - PyDataMem_Handler *mem_handler; PyArrayObject *rop; npy_intp rstride; @@ -1098,7 +1096,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, NPY_BEGIN_THREADS_DEF; - mem_handler = PyDataMem_GetHandler(NULL); + const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(NULL); if (mem_handler == NULL) { return NULL; } diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 5020e294fc06..cb4348ddb5e7 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2099,7 +2099,7 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_RETURN_NONE; } /* Store the functions in case the global handler is modified */ - fa->mem_handler = PyDataMem_GetHandler(NULL); + fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(NULL); if (fa->mem_handler == NULL) { Py_DECREF(rawdata); return NULL; @@ -2157,7 +2157,7 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_RETURN_NONE; } /* Store the functions in case the global handler is modified */ - fa->mem_handler = PyDataMem_GetHandler(NULL); + fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(NULL); if (fa->mem_handler == NULL) { return NULL; } From 99f8250a7418d14dc7530d95edef06cc4a3f7f9f Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 9 Aug 2021 11:30:33 +0300 Subject: [PATCH 27/76] remove import of setuptools==49.1.3, doesn't work on python3.10 --- numpy/testing/_private/extbuild.py | 1 - 1 file changed, 1 deletion(-) diff --git a/numpy/testing/_private/extbuild.py b/numpy/testing/_private/extbuild.py index 53f819be7cc0..1bae6a3c86ae 100644 --- a/numpy/testing/_private/extbuild.py +++ b/numpy/testing/_private/extbuild.py @@ -7,7 +7,6 @@ import os import pathlib import sys -import setuptools # noqa import sysconfig from distutils.ccompiler import new_compiler from distutils.sysconfig import customize_compiler From b43c1feabb799954837f6310b632b4b8263286b8 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 9 Aug 2021 11:54:57 +0300 Subject: [PATCH 28/76] Fix refcount leaks --- numpy/core/src/multiarray/alloc.c | 10 ++++++---- numpy/core/src/multiarray/multiarraymodule.c | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 11d1a42ccf0a..ca130f1ebfbb 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -487,10 +487,11 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) return NULL; } old_handler = (PyDataMem_Handler *) PyCapsule_GetPointer(old_capsule, NULL); + Py_DECREF(old_capsule); if (old_handler == NULL) { return NULL; } - if (handler) { + if (handler != NULL) { capsule = PyCapsule_New(handler, NULL, NULL); if (capsule == NULL) { return NULL; @@ -503,11 +504,10 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) } } token = PyContextVar_Set(current_handler, capsule); + Py_DECREF(capsule); if (token == NULL) { - Py_DECREF(capsule); return NULL; } - Py_DECREF(old_capsule); Py_DECREF(token); return old_handler; } @@ -527,7 +527,9 @@ PyDataMem_GetHandler(PyArrayObject *obj) if (PyContextVar_Get(current_handler, NULL, &capsule)) { return NULL; } - return (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); + handler = (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); + Py_DECREF(capsule); + return handler; } /* If there's a handler, the array owns its own datay */ handler = PyArray_HANDLER(obj); diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index 0bd0042c328c..629c3fb2f929 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -4916,6 +4916,7 @@ PyMODINIT_FUNC PyInit__multiarray_umath(void) { goto err; } current_handler = PyContextVar_New("current_allocator", c_api); + Py_DECREF(c_api); if (current_handler == NULL) { goto err; } From a7a543556807d199913c86584121ac09860353aa Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 9 Aug 2021 12:06:16 +0300 Subject: [PATCH 29/76] fix function signatures in test --- numpy/core/tests/test_mem_policy.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 9285ffeab6db..356bf75c473c 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -42,7 +42,8 @@ def get_module(tmp_path): void (*free)(void *); } Allocator; NPY_NO_EXPORT void * - shift_alloc(Allocator *ctx, size_t sz) { + shift_alloc(void *_ctx, size_t sz) { + Allocator *ctx = (Allocator*)_ctx; char *real = (char *)ctx->malloc(sz + 64); if (real == NULL) { return NULL; @@ -51,7 +52,8 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void * - shift_zero(Allocator *ctx, size_t sz, size_t cnt) { + shift_zero(void *_ctx, size_t sz, size_t cnt) { + Allocator *ctx = (Allocator*)_ctx; char *real = (char *)ctx->calloc(sz + 64, cnt); if (real == NULL) { return NULL; @@ -61,7 +63,8 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void - shift_free(Allocator *ctx, void * p, npy_uintp sz) { + shift_free(void *_ctx, void * p, npy_uintp sz) { + Allocator *ctx = (Allocator*)_ctx; if (p == NULL) { return ; } @@ -87,7 +90,8 @@ def get_module(tmp_path): } } NPY_NO_EXPORT void * - shift_realloc(Allocator *ctx, void * p, npy_uintp sz) { + shift_realloc(void *_ctx, void * p, npy_uintp sz) { + Allocator *ctx = (Allocator*)_ctx; if (p != NULL) { char *real = (char *)p - 64; if (strncmp(real, "originally allocated", 20) != 0) { From e3723df630f3f81d1c7289d1afc17b903723a8bb Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 9 Aug 2021 13:36:41 +0300 Subject: [PATCH 30/76] Return early on PyDataMem_GetHandler error (VOID_compare) --- numpy/core/src/multiarray/arraytypes.c.src | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 086e76aa6e5b..a28974d6fbdf 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3095,9 +3095,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(NULL); if (mem_handler == NULL) { - /* Better fallback to default than goto finish. */ - PyErr_Clear(); - mem_handler = &default_handler; + goto finish; } descr = PyArray_DESCR(ap); /* From 144acc6d1a9ee9f8f08e9aca9fbe24ff0de6f0d6 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 9 Aug 2021 14:40:25 +0300 Subject: [PATCH 31/76] Add context/thread-locality tests, allow testing custom policies --- doc/source/reference/c-api/data_memory.rst | 2 +- numpy/core/src/multiarray/alloc.c | 2 +- numpy/core/tests/test_mem_policy.py | 204 +++++++++++++++------ 3 files changed, 153 insertions(+), 55 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index 237922ab08ee..f7ea8fde9a5f 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -88,7 +88,7 @@ reallocate or free the data memory of the instance. .. c:function:: const PyDataMem_Handler * PyDataMem_GetHandler(PyArrayObject *obj) Return the `PyDataMem_Handler` used by the ``PyArrayObject``. If ``NULL``, - return the current global policy that ill be used to allocate data for the + return the current global policy that will be used to allocate data for the next ``PyArrayObject``. On failure, set an exception and return ``NULL``. For an example of setting up and using the PyDataMem_Handler, see the test in diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index ca130f1ebfbb..fe8bae469ccb 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -514,7 +514,7 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) /*NUMPY_API * Return the PyDataMem_Handler used by the PyArrayObject. If NULL, return - * the current global policy that ill be used to allocate data + * the current global policy that will be used to allocate data * for the next PyArrayObject. On failure, set an exception and return NULL. */ NPY_NO_EXPORT const PyDataMem_Handler * diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 38e7597d9a88..18d2f2d20488 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -1,5 +1,7 @@ +import asyncio import pytest import numpy as np +import threading from numpy.testing import extbuild @@ -11,39 +13,40 @@ def get_module(tmp_path): free/calloc go via the functions here. """ functions = [( - "test_prefix", "METH_O", - """ - if (!PyArray_Check(args)) { - PyErr_SetString(PyExc_ValueError, - "must be called with a numpy scalar or ndarray"); - } - return PyUnicode_FromString( - PyDataMem_GetHandler((PyArrayObject*)args)->name); - """ - ), - ("set_new_policy", "METH_NOARGS", + "set_secret_data_policy", "METH_NOARGS", """ - const PyDataMem_Handler *old = PyDataMem_SetHandler(&new_handler); - return PyUnicode_FromString(old->name); + PyDataMem_Handler *old = (PyDataMem_Handler *) PyDataMem_SetHandler(&secret_data_handler); + return PyCapsule_New(old, NULL, NULL); """), - ("set_old_policy", "METH_NOARGS", + ("set_old_policy", "METH_O", """ - const PyDataMem_Handler *old = PyDataMem_SetHandler(NULL); - return PyUnicode_FromString(old->name); + PyDataMem_Handler *old = NULL; + if (args != NULL && PyCapsule_CheckExact(args)) { + old = (PyDataMem_Handler *) PyCapsule_GetPointer(args, NULL); + } + PyDataMem_SetHandler(old); + Py_RETURN_NONE; """), ] prologue = ''' #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include + /* + * This struct allows the dynamic configuration of the allocator funcs + * of the `secret_data_allocator`. It is provided here for + * demonstration purposes, as a valid `ctx` use-case scenario. + */ typedef struct { void *(*malloc)(size_t); void *(*calloc)(size_t, size_t); void *(*realloc)(void *, size_t); void (*free)(void *); - } Allocator; + } SecretDataAllocatorFuncs; NPY_NO_EXPORT void * - shift_alloc(Allocator *ctx, size_t sz) { - char *real = (char *)ctx->malloc(sz + 64); + shift_alloc(void *ctx, size_t sz) { + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *) ctx; + + char *real = (char *)funcs->malloc(sz + 64); if (real == NULL) { return NULL; } @@ -51,8 +54,10 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void * - shift_zero(Allocator *ctx, size_t sz, size_t cnt) { - char *real = (char *)ctx->calloc(sz + 64, cnt); + shift_zero(void *ctx, size_t sz, size_t cnt) { + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *) ctx; + + char *real = (char *)funcs->calloc(sz + 64, cnt); if (real == NULL) { return NULL; } @@ -61,7 +66,9 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void - shift_free(Allocator *ctx, void * p, npy_uintp sz) { + shift_free(void *ctx, void * p, npy_uintp sz) { + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *) ctx; + if (p == NULL) { return ; } @@ -70,8 +77,8 @@ def get_module(tmp_path): fprintf(stdout, "uh-oh, unmatched shift_free, " "no appropriate prefix\\n"); /* Make C runtime crash by calling free on the wrong address */ - ctx->free((char *)p + 10); - /* ctx->free(real); */ + funcs->free((char *)p + 10); + /* funcs->free(real); */ } else { npy_uintp i = (npy_uintp)atoi(real +20); @@ -79,25 +86,27 @@ def get_module(tmp_path): fprintf(stderr, "uh-oh, unmatched shift_free" "(ptr, %ld) but allocated %ld\\n", sz, i); /* This happens in some places, only print */ - ctx->free(real); + funcs->free(real); } else { - ctx->free(real); + funcs->free(real); } } } NPY_NO_EXPORT void * - shift_realloc(Allocator *ctx, void * p, npy_uintp sz) { + shift_realloc(void *ctx, void * p, npy_uintp sz) { + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *) ctx; + if (p != NULL) { char *real = (char *)p - 64; if (strncmp(real, "originally allocated", 20) != 0) { fprintf(stdout, "uh-oh, unmatched shift_realloc\\n"); return realloc(p, sz); } - return (void *)((char *)ctx->realloc(real, sz + 64) + 64); + return (void *)((char *)funcs->realloc(real, sz + 64) + 64); } else { - char *real = (char *)ctx->realloc(p, sz + 64); + char *real = (char *)funcs->realloc(p, sz + 64); if (real == NULL) { return NULL; } @@ -106,20 +115,21 @@ def get_module(tmp_path): return (void *)(real + 64); } } - static Allocator new_handler_ctx = { + /* As an example, we use the standard {m|c|re}alloc/free funcs. */ + static SecretDataAllocatorFuncs secret_data_handler_ctx = { malloc, calloc, realloc, free }; - static PyDataMem_Handler new_handler = { + static PyDataMem_Handler secret_data_handler = { "secret_data_allocator", { - &new_handler_ctx, - shift_alloc, /* malloc */ - shift_zero, /* calloc */ - shift_realloc, /* realloc */ - shift_free /* free */ + &secret_data_handler_ctx, /* ctx */ + shift_alloc, /* malloc */ + shift_zero, /* calloc */ + shift_realloc, /* realloc */ + shift_free /* free */ } }; ''' @@ -138,25 +148,112 @@ def get_module(tmp_path): def test_set_policy(get_module): - a = np.arange(10) - orig_policy = get_module.test_prefix(a) - assert orig_policy == np.core.multiarray.get_handler_name() - assert orig_policy == np.core.multiarray.get_handler_name(a) - assert get_module.set_new_policy() == orig_policy - if orig_policy == 'default_allocator': - get_module.set_old_policy() + orig_policy_name = np.core.multiarray.get_handler_name() + + a = np.arange(10).reshape((2, 5)) # a doesn't own its own data + assert np.core.multiarray.get_handler_name(a) == orig_policy_name + + orig_policy = get_module.set_secret_data_policy() + + b = np.arange(10).reshape((2, 5)) # b doesn't own its own data + assert np.core.multiarray.get_handler_name(b) == 'secret_data_allocator' + + if orig_policy_name == 'default_allocator': + get_module.set_old_policy(None) + + assert np.core.multiarray.get_handler_name() == 'default_allocator' + else: + get_module.set_old_policy(orig_policy) + + assert np.core.multiarray.get_handler_name() == orig_policy_name + + +async def concurrent_context1(get_module, event): + get_module.set_secret_data_policy() + + assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' + + event.set() + + +async def concurrent_context2(get_module, orig_policy_name, event): + await event.wait() + + assert np.core.multiarray.get_handler_name() == orig_policy_name + + +async def secret_data_context(get_module): + assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' + + get_module.set_old_policy(None) + + +async def async_test_context_locality(get_module): + orig_policy_name = np.core.multiarray.get_handler_name() + + event = asyncio.Event() + concurrent_task1 = asyncio.create_task(concurrent_context1(get_module, event)) + concurrent_task2 = asyncio.create_task(concurrent_context2(get_module, orig_policy_name, event)) + await concurrent_task1 + await concurrent_task2 + + assert np.core.multiarray.get_handler_name() == orig_policy_name + + orig_policy = get_module.set_secret_data_policy() + + await asyncio.create_task(secret_data_context(get_module)) + + assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' + + get_module.set_old_policy(orig_policy) + + +def test_context_locality(get_module): + asyncio.run(async_test_context_locality(get_module)) + + +def concurrent_thread1(get_module, event): + assert np.core.multiarray.get_handler_name() == 'default_allocator' + + get_module.set_secret_data_policy() + + assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' + + event.set() + + +def concurrent_thread2(get_module, event): + event.wait() + + assert np.core.multiarray.get_handler_name() == 'default_allocator' + + +def test_thread_locality(get_module): + orig_policy_name = np.core.multiarray.get_handler_name() + + event = threading.Event() + concurrent_task1 = threading.Thread(target=concurrent_thread1, args=(get_module, event)) + concurrent_task2 = threading.Thread(target=concurrent_thread2, args=(get_module, event)) + concurrent_task1.start() + concurrent_task2.start() + concurrent_task1.join() + concurrent_task2.join() + + assert np.core.multiarray.get_handler_name() == orig_policy_name @pytest.mark.slow def test_new_policy(get_module): a = np.arange(10) - orig_policy = get_module.test_prefix(a) - assert get_module.set_new_policy() == orig_policy - b = np.arange(10).reshape((2, 5)) - assert get_module.test_prefix(b) == 'secret_data_allocator' + orig_policy_name = np.core.multiarray.get_handler_name(a) + + orig_policy = get_module.set_secret_data_policy() + + b = np.arange(10) + assert np.core.multiarray.get_handler_name(b) == 'secret_data_allocator' # test array manipulation. This is slow - if orig_policy == 'default_allocator': + if orig_policy_name == 'default_allocator': # when the np.core.test tests recurse into this test, the # policy will be set so this "if" will be false, preventing # infinite recursion @@ -164,10 +261,11 @@ def test_new_policy(get_module): # if needed, debug this by # - running tests with -- -s (to not capture stdout/stderr # - setting extra_argv=['-vv'] here - np.core.test(verbose=2, extra_argv=['-vv']) + assert np.core.test('full', verbose=2, extra_argv=['-vv']) # also try the ma tests, the pickling test is quite tricky - np.ma.test(verbose=2, extra_argv=['-vv']) - get_module.set_old_policy() - assert get_module.test_prefix(a) == orig_policy + assert np.ma.test('full', verbose=2, extra_argv=['-vv']) + + get_module.set_old_policy(orig_policy) + c = np.arange(10) - assert get_module.test_prefix(c) == 'default_allocator' + assert np.core.multiarray.get_handler_name(c) == orig_policy_name From 6ab00d06033f87932f4123c9d7a6eff1a28312c7 Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 11 Oct 2020 14:41:41 +0300 Subject: [PATCH 32/76] ENH: add and use global configurable memory routines --- numpy/core/code_generators/cversions.txt | 3 + numpy/core/code_generators/numpy_api.py | 3 + numpy/core/include/numpy/ndarraytypes.h | 35 +++- numpy/core/setup_common.py | 4 +- numpy/core/src/multiarray/alloc.c | 161 ++++++++++++++++++- numpy/core/src/multiarray/alloc.h | 13 +- numpy/core/src/multiarray/arrayobject.c | 3 +- numpy/core/src/multiarray/arraytypes.c.src | 15 +- numpy/core/src/multiarray/convert_datatype.c | 2 +- numpy/core/src/multiarray/ctors.c | 26 ++- numpy/core/src/multiarray/item_selection.c | 18 ++- numpy/core/src/multiarray/scalartypes.c.src | 14 +- numpy/core/src/multiarray/shape.c | 5 +- 13 files changed, 257 insertions(+), 45 deletions(-) diff --git a/numpy/core/code_generators/cversions.txt b/numpy/core/code_generators/cversions.txt index a02c7153a5c6..4a2e68525612 100644 --- a/numpy/core/code_generators/cversions.txt +++ b/numpy/core/code_generators/cversions.txt @@ -58,3 +58,6 @@ # Version 14 (NumPy 1.21) No change. # Version 14 (NumPy 1.22) No change. 0x0000000e = 17a0f366e55ec05e5c5c149123478452 + +# Version 15 (NumPy 1.20) Configurable memory allocations +0x0000000f = 4177738910303368a00be8b1ce9283d5 diff --git a/numpy/core/code_generators/numpy_api.py b/numpy/core/code_generators/numpy_api.py index fbd3233680fa..a915bbb35b5f 100644 --- a/numpy/core/code_generators/numpy_api.py +++ b/numpy/core/code_generators/numpy_api.py @@ -350,6 +350,9 @@ 'PyArray_ResolveWritebackIfCopy': (302,), 'PyArray_SetWritebackIfCopyBase': (303,), # End 1.14 API + 'PyDataMem_SetHandler': (304,), + 'PyDataMem_GetHandlerName': (305,), + # End 1.20 API } ufunc_types_api = { diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index 84441e641d7c..31b09c371c78 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -355,12 +355,10 @@ struct NpyAuxData_tag { #define NPY_ERR(str) fprintf(stderr, #str); fflush(stderr); #define NPY_ERR2(str) fprintf(stderr, str); fflush(stderr); - /* - * Macros to define how array, and dimension/strides data is - * allocated. - */ - - /* Data buffer - PyDataMem_NEW/FREE/RENEW are in multiarraymodule.c */ +/* +* Macros to define how array, and dimension/strides data is +* allocated. These should be made private +*/ #define NPY_USE_PYMEM 1 @@ -666,6 +664,27 @@ typedef struct _arr_descr { PyObject *shape; /* a tuple */ } PyArray_ArrayDescr; +/* + * Memory handler structure for array data. + */ +typedef void *(PyDataMem_AllocFunc)(size_t size); +typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); +typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); +typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); +typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); + +typedef struct { + char name[128]; /* multiple of 64 to keep the struct unaligned */ + PyDataMem_AllocFunc *alloc; + PyDataMem_ZeroedAllocFunc *zeroed_alloc; + PyDataMem_FreeFunc *free; + PyDataMem_ReallocFunc *realloc; + PyDataMem_CopyFunc *host2obj; /* copy from the host python */ + PyDataMem_CopyFunc *obj2host; /* copy to the host python */ + PyDataMem_CopyFunc *obj2obj; /* copy between two objects */ +} PyDataMem_Handler; + + /* * The main array object structure. * @@ -716,6 +735,10 @@ typedef struct tagPyArrayObject_fields { /* For weak references */ PyObject *weakreflist; void *_buffer_info; /* private buffer info, tagged to allow warning */ + /* + * For alloc/malloc/realloc/free/memcpy per object + */ + PyDataMem_Handler *mem_handler; } PyArrayObject_fields; /* diff --git a/numpy/core/setup_common.py b/numpy/core/setup_common.py index 85c8f16d1e73..70e8fc89705a 100644 --- a/numpy/core/setup_common.py +++ b/numpy/core/setup_common.py @@ -43,8 +43,8 @@ # 0x0000000d - 1.19.x # 0x0000000e - 1.20.x # 0x0000000e - 1.21.x -# 0x0000000e - 1.22.x -C_API_VERSION = 0x0000000e +# 0x0000000f - 1.22.x +C_API_VERSION = 0x0000000f class MismatchCAPIWarning(Warning): pass diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 887deff53457..295c248565d7 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -140,9 +140,10 @@ npy_alloc_cache(npy_uintp sz) /* zero initialized data, sz is number of bytes to allocate */ NPY_NO_EXPORT void * -npy_alloc_cache_zero(npy_uintp sz) +npy_alloc_cache_zero(size_t nmemb, size_t size) { void * p; + size_t sz = nmemb * size; NPY_BEGIN_THREADS_DEF; if (sz < NBUCKETS) { p = _npy_alloc_cache(sz, 1, NBUCKETS, datacache, &PyDataMem_NEW); @@ -152,7 +153,7 @@ npy_alloc_cache_zero(npy_uintp sz) return p; } NPY_BEGIN_THREADS; - p = PyDataMem_NEW_ZEROED(sz, 1); + p = PyDataMem_NEW_ZEROED(nmemb, size); NPY_END_THREADS; return p; } @@ -261,21 +262,21 @@ PyDataMem_NEW(size_t size) * Allocates zeroed memory for array data. */ NPY_NO_EXPORT void * -PyDataMem_NEW_ZEROED(size_t size, size_t elsize) +PyDataMem_NEW_ZEROED(size_t nmemb, size_t size) { void *result; - result = calloc(size, elsize); + result = calloc(nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API if (_PyDataMem_eventhook != NULL) { - (*_PyDataMem_eventhook)(NULL, result, size * elsize, + (*_PyDataMem_eventhook)(NULL, result, nmemb * size, _PyDataMem_eventhook_user_data); } NPY_DISABLE_C_API } - PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, size); + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, nmemb * size); return result; } @@ -323,3 +324,151 @@ PyDataMem_RENEW(void *ptr, size_t size) } return result; } + +typedef void *(alloc_wrapper)(size_t, PyDataMem_AllocFunc *); +typedef void *(zalloc_wrapper)(size_t nelems, size_t elsize); +typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); +typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); +typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); + +/* Memory handler global default */ +static PyDataMem_Handler default_allocator = { + "default_allocator", + npy_alloc_cache, /* alloc */ + npy_alloc_cache_zero, /* zeroed_alloc */ + npy_free_cache, /* free */ + PyDataMem_RENEW, /* realloc */ + memcpy, /* host2obj */ + memcpy, /* obj2host */ + memcpy, /* obj2obj */ +}; + +PyDataMem_Handler *current_allocator = &default_allocator; + +int uo_index=0; /* user_override index */ + +/* Wrappers for user-assigned PyDataMem_Handlers */ + +NPY_NO_EXPORT void * +PyDataMem_UserNEW(size_t size, PyDataMem_AllocFunc *alloc) +{ + void *result; + + if (alloc == npy_alloc_cache) { + // All the logic below is conditionally handled by npy_alloc_cache + return npy_alloc_cache(size); + } + assert(size != 0); + result = alloc(size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(NULL, result, size, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, size); + return result; +} + +NPY_NO_EXPORT void * +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *zalloc) +{ + void *result; + if (zalloc == npy_alloc_cache_zero) { + // All the logic below is conditionally handled by npy_alloc_cache_zero + return npy_alloc_cache_zero(nmemb, size); + } + + result = zalloc(nmemb, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(NULL, result, nmemb * size, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, nmemb * size); + return result; +} + +NPY_NO_EXPORT void +PyDataMem_UserFREE(void *ptr, size_t size, PyDataMem_FreeFunc *func) +{ + if (func == npy_free_cache) { + // All the logic below is conditionally handled by npy_free_cache + return npy_free_cache(ptr, size); + } + PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); + func(ptr, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(ptr, NULL, 0, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } +} + +NPY_NO_EXPORT void * +PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func) +{ + void *result; + + assert(size != 0); + result = func(ptr, size); + if (result != ptr) { + PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); + } + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(ptr, result, size, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } + return result; +} + +/*NUMPY_API + * Sets a new allocation policy. If the input value is NULL, will reset + * the policy to the default. Returns the previous policy, NULL if the + * previous policy was the default. We wrap the user-provided functions + * so they will still call the python and numpy memory management callback + * hooks. + */ +NPY_NO_EXPORT const PyDataMem_Handler * +PyDataMem_SetHandler(PyDataMem_Handler *handler) +{ + const PyDataMem_Handler *old = current_allocator; + if (handler) { + current_allocator = handler; + } + else { + current_allocator = &default_allocator; + } + return old; +} + +/*NUMPY_API + * Return the const char name of the PyDataMem_Handler used by the + * PyArrayObject. If NULL, return the name of the current global policy that + * will be used to allocate data for the next PyArrayObject + */ +NPY_NO_EXPORT const char * +PyDataMem_GetHandlerName(PyArrayObject *obj) +{ + if (obj == NULL) { + return current_allocator->name; + } + return PyArray_HANDLER(obj)->name; +} diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index 15e31ebb5f2f..5a1726090358 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -10,13 +10,16 @@ NPY_NO_EXPORT PyObject * _set_madvise_hugepage(PyObject *NPY_UNUSED(self), PyObject *enabled_obj); NPY_NO_EXPORT void * -npy_alloc_cache(npy_uintp sz); +PyDataMem_UserNEW(npy_uintp sz, PyDataMem_AllocFunc *alloc); NPY_NO_EXPORT void * -npy_alloc_cache_zero(npy_uintp sz); +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *zalloc); NPY_NO_EXPORT void -npy_free_cache(void * p, npy_uintp sd); +PyDataMem_UserFREE(void * p, npy_uintp sd, PyDataMem_FreeFunc *func); + +NPY_NO_EXPORT void * +PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func); NPY_NO_EXPORT void * npy_alloc_cache_dim(npy_uintp sz); @@ -36,4 +39,8 @@ npy_free_cache_dim_array(PyArrayObject * arr) npy_free_cache_dim(PyArray_DIMS(arr), PyArray_NDIM(arr)); } +extern PyDataMem_Handler *current_allocator; + +#define PyArray_HANDLER(arr) ((PyArrayObject_fields*)(arr))->mem_handler + #endif diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index 55ba5601b4ed..b439c11374fd 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -493,7 +493,8 @@ array_dealloc(PyArrayObject *self) if (PyDataType_FLAGCHK(fa->descr, NPY_ITEM_REFCOUNT)) { PyArray_XDECREF(self); } - npy_free_cache(fa->data, PyArray_NBYTES(self)); + PyDataMem_UserFREE(fa->data, PyArray_NBYTES(self), + fa->mem_handler->free); } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index b3ea7544d974..22c62c20d319 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3107,15 +3107,17 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (_unpack_field(tup, &new, &offset) < 0) { goto finish; } - /* descr is the only field checked by compare or copyswap */ + /* Set the fields needed by compare or copyswap */ dummy_struct.descr = new; + dummy_struct.mem_handler = PyArray_HANDLER(ap); + swap = PyArray_ISBYTESWAPPED(dummy); nip1 = ip1 + offset; nip2 = ip2 + offset; if (swap || new->alignment > 1) { if (swap || !npy_is_aligned(nip1, new->alignment)) { /* create buffer and copy */ - nip1 = npy_alloc_cache(new->elsize); + nip1 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->alloc); if (nip1 == NULL) { goto finish; } @@ -3125,10 +3127,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } if (swap || !npy_is_aligned(nip2, new->alignment)) { /* create buffer and copy */ - nip2 = npy_alloc_cache(new->elsize); + nip2 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->alloc); if (nip2 == NULL) { if (nip1 != ip1 + offset) { - npy_free_cache(nip1, new->elsize); + PyDataMem_UserFREE(nip1, new->elsize, + PyArray_HANDLER(ap)->free); } goto finish; } @@ -3140,10 +3143,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, dummy); if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { - npy_free_cache(nip1, new->elsize); + PyDataMem_UserFREE(nip1, new->elsize, PyArray_HANDLER(ap)->free); } if (nip2 != ip2 + offset) { - npy_free_cache(nip2, new->elsize); + PyDataMem_UserFREE(nip2, new->elsize, PyArray_HANDLER(ap)->free); } } if (res != 0) { diff --git a/numpy/core/src/multiarray/convert_datatype.c b/numpy/core/src/multiarray/convert_datatype.c index e3b25d0763b6..d89553293934 100644 --- a/numpy/core/src/multiarray/convert_datatype.c +++ b/numpy/core/src/multiarray/convert_datatype.c @@ -2097,7 +2097,7 @@ PyArray_ObjectType(PyObject *op, int minimum_type) * This function is only used in one place within NumPy and should * generally be avoided. It is provided mainly for backward compatibility. * - * The user of the function has to free the returned array. + * The user of the function has to free the returned array with PyDataMem_FREE. */ NPY_NO_EXPORT PyArrayObject ** PyArray_ConvertToCommonType(PyObject *op, int *retn) diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index aaa645c1650d..f4161bfb0689 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -804,6 +804,9 @@ PyArray_NewFromDescr_int( fa->flags |= NPY_ARRAY_C_CONTIGUOUS|NPY_ARRAY_F_CONTIGUOUS; } + /* Store the functions in case the global hander is modified */ + fa->mem_handler = current_allocator; + if (data == NULL) { /* * Allocate something even for zero-space arrays @@ -819,15 +822,17 @@ PyArray_NewFromDescr_int( * which could also be sub-fields of a VOID array */ if (zeroed || PyDataType_FLAGCHK(descr, NPY_NEEDS_INIT)) { - data = npy_alloc_cache_zero(nbytes); + data = PyDataMem_UserNEW_ZEROED(nbytes, 1, + fa->mem_handler->zeroed_alloc); } else { - data = npy_alloc_cache(nbytes); + data = PyDataMem_UserNEW(nbytes, fa->mem_handler->alloc); } if (data == NULL) { raise_memory_error(fa->nd, fa->dimensions, descr); goto fail; } + fa->flags |= NPY_ARRAY_OWNDATA; } else { @@ -3401,7 +3406,8 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre dptr += dtype->elsize; if (num < 0 && thisbuf == size) { totalbytes += bytes; - tmp = PyDataMem_RENEW(PyArray_DATA(r), totalbytes); + tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes, + PyArray_HANDLER(r)->realloc); if (tmp == NULL) { err = 1; break; @@ -3423,7 +3429,8 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre const size_t nsize = PyArray_MAX(*nread,1)*dtype->elsize; if (nsize != 0) { - tmp = PyDataMem_RENEW(PyArray_DATA(r), nsize); + tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize, + PyArray_HANDLER(r)->realloc); if (tmp == NULL) { err = 1; } @@ -3528,7 +3535,8 @@ PyArray_FromFile(FILE *fp, PyArray_Descr *dtype, npy_intp num, char *sep) const size_t nsize = PyArray_MAX(nread,1) * dtype->elsize; char *tmp; - if ((tmp = PyDataMem_RENEW(PyArray_DATA(ret), nsize)) == NULL) { + if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize, + PyArray_HANDLER(ret)->realloc)) == NULL) { Py_DECREF(dtype); Py_DECREF(ret); return PyErr_NoMemory(); @@ -3812,7 +3820,8 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) */ elcount = (i >> 1) + (i < 4 ? 4 : 2) + i; if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) { - new_data = PyDataMem_RENEW(PyArray_DATA(ret), nbytes); + new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes, + PyArray_HANDLER(ret)->realloc); } else { new_data = NULL; @@ -3850,10 +3859,11 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) * (assuming realloc is reasonably good about reusing space...) */ if (i == 0 || elsize == 0) { - /* The size cannot be zero for PyDataMem_RENEW. */ + /* The size cannot be zero for realloc. */ goto done; } - new_data = PyDataMem_RENEW(PyArray_DATA(ret), i * elsize); + new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize, + PyArray_HANDLER(ret)->realloc); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate array memory"); diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 2b8ea9e79ace..1d48916098a9 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -776,6 +776,7 @@ PyArray_Repeat(PyArrayObject *aop, PyObject *op, int axis) return NULL; } + /*NUMPY_API */ NPY_NO_EXPORT PyObject * @@ -907,7 +908,7 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out, Py_XDECREF(mps[i]); } Py_DECREF(ap); - npy_free_cache(mps, n * sizeof(mps[0])); + PyDataMem_FREE(mps); if (out != NULL && out != obj) { Py_INCREF(out); PyArray_ResolveWritebackIfCopy(obj); @@ -922,7 +923,7 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out, Py_XDECREF(mps[i]); } Py_XDECREF(ap); - npy_free_cache(mps, n * sizeof(mps[0])); + PyDataMem_FREE(mps); PyArray_DiscardWritebackIfCopy(obj); Py_XDECREF(obj); return NULL; @@ -969,7 +970,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, size = it->size; if (needcopy) { - buffer = npy_alloc_cache(N * elsize); + buffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->alloc); if (buffer == NULL) { ret = -1; goto fail; @@ -1053,7 +1054,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - npy_free_cache(buffer, N * elsize); + PyDataMem_UserFREE(buffer, N * elsize, PyArray_HANDLER(op)->free); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -1115,7 +1116,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = npy_alloc_cache(N * elsize); + valbuffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->alloc); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1123,7 +1124,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, } if (needidxbuffer) { - idxbuffer = (npy_intp *)npy_alloc_cache(N * sizeof(npy_intp)); + idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), + PyArray_HANDLER(op)->alloc); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1212,8 +1214,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - npy_free_cache(valbuffer, N * elsize); - npy_free_cache(idxbuffer, N * sizeof(npy_intp)); + PyDataMem_UserFREE(valbuffer, N * elsize, PyArray_HANDLER(op)->free); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), PyArray_HANDLER(op)->free); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index 40f736125de3..2d581845eb98 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -34,6 +34,16 @@ #include "binop_override.h" +/* + * used for allocating a single scalar, so use the default numpy + * memory allocators instead of the (maybe) user overrides + */ +NPY_NO_EXPORT void * +npy_alloc_cache_zero(size_t nmemb, size_t size); + +NPY_NO_EXPORT void +npy_free_cache(void * p, npy_uintp sz); + NPY_NO_EXPORT PyBoolScalarObject _PyArrayScalar_BoolValues[] = { {PyObject_HEAD_INIT(&PyBoolArrType_Type) 0}, {PyObject_HEAD_INIT(&PyBoolArrType_Type) 1}, @@ -1321,7 +1331,7 @@ gentype_imag_get(PyObject *self, void *NPY_UNUSED(ignored)) int elsize; typecode = PyArray_DescrFromScalar(self); elsize = typecode->elsize; - temp = npy_alloc_cache_zero(elsize); + temp = npy_alloc_cache_zero(1, elsize); ret = PyArray_Scalar(temp, typecode, NULL); npy_free_cache(temp, elsize); } @@ -3022,7 +3032,7 @@ void_arrtype_new(PyTypeObject *type, PyObject *args, PyObject *kwds) (int) NPY_MAX_INT); return NULL; } - destptr = npy_alloc_cache_zero(memu); + destptr = npy_alloc_cache_zero(memu, 1); if (destptr == NULL) { return PyErr_NoMemory(); } diff --git a/numpy/core/src/multiarray/shape.c b/numpy/core/src/multiarray/shape.c index 02c349759528..a67063987c89 100644 --- a/numpy/core/src/multiarray/shape.c +++ b/numpy/core/src/multiarray/shape.c @@ -120,8 +120,9 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, } /* Reallocate space if needed - allocating 0 is forbidden */ - new_data = PyDataMem_RENEW( - PyArray_DATA(self), newnbytes == 0 ? elsize : newnbytes); + new_data = PyDataMem_UserRENEW(PyArray_DATA(self), + newnbytes == 0 ? elsize : newnbytes, + PyArray_HANDLER(self)->realloc); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory for array"); From e7e8754b4d201198a7f5a36658424d195cdc916c Mon Sep 17 00:00:00 2001 From: mattip Date: Fri, 16 Oct 2020 16:23:00 +0300 Subject: [PATCH 33/76] ENH: add tests and a way to compile c-extensions from tests --- doc/TESTS.rst.txt | 15 ++ numpy/core/tests/test_mem_policy.py | 143 ++++++++++++++++ numpy/testing/__init__.py | 2 +- numpy/testing/_private/extbuild.py | 248 ++++++++++++++++++++++++++++ 4 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 numpy/core/tests/test_mem_policy.py create mode 100644 numpy/testing/_private/extbuild.py diff --git a/doc/TESTS.rst.txt b/doc/TESTS.rst.txt index ba09aa80028a..0b9cc610eb1e 100644 --- a/doc/TESTS.rst.txt +++ b/doc/TESTS.rst.txt @@ -145,6 +145,21 @@ originally written without unit tests, there are still several modules that don't have tests yet. Please feel free to choose one of these modules and develop tests for it. +Using C code in tests +--------------------- + +NumPy exposes a rich :ref:`C-API` . These are tested using c-extension +modules written "as-if" they know nothing about the internals of NumPy, rather +using the official C-API interfaces only. Examples of such modules are tests +for a user-defined ``rational`` dtype in ``_rational_tests`` or the ufunc +machinery tests in ``_umath_tests`` which are part of the binary distribution. +Starting from version 1.20, you can also write snippets of C code in tests that +will be compiled locally into c-extension modules and loaded into python. + +.. currentmodule:: numpy.testing.extbuild + +.. autofunction:: build_and_import_extension + Labeling tests -------------- diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py new file mode 100644 index 000000000000..f6320f66410c --- /dev/null +++ b/numpy/core/tests/test_mem_policy.py @@ -0,0 +1,143 @@ +import pathlib +import pytest +import tempfile +import numpy as np +from numpy.testing import extbuild + +@pytest.fixture +def get_module(tmp_path): + """ Add a memory policy that returns a false pointer 64 bytes into the + actual allocation, and fill the prefix with some text. Then check at each + memory manipulation that the prefix exists, to make sure all alloc/realloc/ + free/calloc go via the functions here. + """ + functions = [( + "test_prefix", "METH_O", + """ + if (!PyArray_Check(args)) { + PyErr_SetString(PyExc_ValueError, + "must be called with a numpy scalar or ndarray"); + } + return PyUnicode_FromString(PyDataMem_GetHandlerName((PyArrayObject*)args)); + """ + ), + ("set_new_policy", "METH_NOARGS", + """ + const PyDataMem_Handler *old = PyDataMem_SetHandler(&new_handler); + return PyUnicode_FromString(old->name); + """), + ("set_old_policy", "METH_NOARGS", + """ + const PyDataMem_Handler *old = PyDataMem_SetHandler(NULL); + return PyUnicode_FromString(old->name); + """), + ] + prologue=''' + #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION + #include + NPY_NO_EXPORT void * + shift_alloc(size_t sz) { + char *real = (char *)malloc(sz + 64); + if (real == NULL) { + return NULL; + } + snprintf(real, 64, "originally allocated %ld", sz); + return (void *)(real + 64); + } + NPY_NO_EXPORT void * + shift_zero(size_t sz, size_t cnt) { + char *real = (char *)calloc(sz + 64, cnt); + if (real == NULL) { + return NULL; + } + snprintf(real, 64, "originally allocated %ld", sz); + return (void *)(real + 64); + } + NPY_NO_EXPORT void + shift_free(void * p, npy_uintp sz) { + if (p == NULL) { + return ; + } + char *real = (char *)p - 64; + if (strncmp(real, "originally allocated", 20) != 0) { + fprintf(stdout, "uh-oh, unmatched shift_free\\n"); + /* Make gcc crash by calling free on the wrong address */ + free((char *)p + 10); + /* free(p); */ + } + else { + free(real); + } + } + NPY_NO_EXPORT void * + shift_realloc(void * p, npy_uintp sz) { + if (p != NULL) { + char *real = (char *)p - 64; + if (strncmp(real, "originally allocated", 20) != 0) { + fprintf(stdout, "uh-oh, unmatched shift_realloc\\n"); + return realloc(p, sz); + } + return (void *)((char *)realloc(real, sz + 64) + 64); + } + else { + char *real = (char *)realloc(p, sz + 64); + if (real == NULL) { + return NULL; + } + snprintf(real, 64, "originally allocated (realloc) %ld", sz); + return (void *)(real + 64); + } + } + static PyDataMem_Handler new_handler = { + "secret_data_allocator", + shift_alloc, /* alloc */ + shift_zero, /* zeroed_alloc */ + shift_free, /* free */ + shift_realloc, /* realloc */ + memcpy, /* host2obj */ + memcpy, /* obj2host */ + memcpy, /* obj2obj */ + }; + ''' + more_init="import_array();" + try: + import mem_policy + return mem_policy + except ImportError: + pass + # if it does not exist, build and load it + try: + return extbuild.build_and_import_extension('mem_policy', + functions, prologue=prologue, include_dirs=[np.get_include()], + build_dir=tmp_path, more_init=more_init) + except: + raise + pytest.skip("could not build module") + + +def test_set_policy(get_module): + a = np.arange(10) + orig_policy = get_module.test_prefix(a) + assert get_module.set_new_policy() == orig_policy + if orig_policy == 'default_allocator': + get_module.set_old_policy() + +@pytest.mark.slow +def test_new_policy(get_module): + a = np.arange(10) + orig_policy = get_module.test_prefix(a) + assert get_module.set_new_policy() == orig_policy + b = np.arange(10) + assert get_module.test_prefix(b) == 'secret_data_allocator' + + # test array manipulation. This is slow + if orig_policy == 'default_allocator': + # when the test set recurses into this test, the policy will be set + # so this "if" will be false, preventing infinite recursion + # + # if needed, debug this by setting extra_argv=['-vvx'] + np.core.test(verbose=0, extra_argv=[]) + get_module.set_old_policy() + assert get_module.test_prefix(a) == orig_policy + c = np.arange(10) + assert get_module.test_prefix(c) == 'default_allocator' diff --git a/numpy/testing/__init__.py b/numpy/testing/__init__.py index bca1d3670233..a008f5828e58 100644 --- a/numpy/testing/__init__.py +++ b/numpy/testing/__init__.py @@ -10,7 +10,7 @@ from ._private.utils import * from ._private.utils import (_assert_valid_refcount, _gen_alignment_data, IS_PYSTON) -from ._private import decorators as dec +from ._private import extbuild, decorators as dec from ._private.nosetester import ( run_module_suite, NoseTester as Tester ) diff --git a/numpy/testing/_private/extbuild.py b/numpy/testing/_private/extbuild.py new file mode 100644 index 000000000000..a94ca94c1821 --- /dev/null +++ b/numpy/testing/_private/extbuild.py @@ -0,0 +1,248 @@ +""" +Build a c-extension module on-the-fly in tests. +See build_and_import_extensions for usage hints + +""" + +import os +import pathlib +import sys +import setuptools +import sysconfig +from distutils.ccompiler import new_compiler +from distutils.sysconfig import customize_compiler +from distutils.errors import CompileError + +__all__ = ['build_and_import_extensions', 'compile_extension_module'] + +def build_and_import_extension(modname, functions, *, prologue="", build_dir=None, + include_dirs=[], more_init="", + ): + """ + Build and imports a c-extension module `modname` from a list of function + fragments `functions`. + + + Parameters + ---------- + functions : list of fragments + Each fragment is a sequence of func_name, calling convention, snippet. + prologue : string + Code to preceed the rest, usually extra ``#include`` or ``#define`` + macros. + build_dir : pathlib.Path + Where to build the module, usually a temporary directory + include_dirs : list + Extra directories to find include files when compiling + more_init : string + Code to appear in the module PyMODINIT_FUNC + + Returns + ------- + out: module + The module will have been loaded and is ready for use + + Examples + -------- + >>> functions = [("test_bytes", "METH_O", \"\"\" + if ( !PyBytesCheck(args)) { + Py_RETURN_FALSE; + } + Py_RETURN_TRUE; + \"\"\")] + >>> mod = build_and_import_extension("testme", functions) + >>> assert not mod.test_bytes(u'abc') + >>> assert mod.test_bytes(b'abc') + """ + + body = prologue + _make_methods(functions, modname) + init = """PyObject *mod = PyModule_Create(&moduledef); + """ + if not build_dir: + build_dir = pathlib.Path('.') + if more_init: + init += """#define INITERROR return NULL + """ + init += more_init + init += "\nreturn mod;" + source_string = _make_source(modname, init, body) + try: + mod_so = compile_extension_module( + modname, build_dir, include_dirs, source_string) + except CompileError as e: + # shorten the exception chain + raise RuntimeError(f"could not compile in {build_dir}:") from e + import importlib.util + spec = importlib.util.spec_from_file_location(modname, mod_so) + foo = importlib.util.module_from_spec(spec) + spec.loader.exec_module(foo) + return foo + +def compile_extension_module(name, builddir, include_dirs, + source_string, libraries=[], library_dirs=[]): + """ + Build an extension module and return the filename of the resulting + native code file. + + Parameters + ---------- + name : string + name of the module, possibly including dots if it is a module inside a + package. + builddir : pathlib.Path + Where to build the module, usually a temporary directory + include_dirs : list + Extra directories to find include files when compiling + libraries : list + Libraries to link into the extension module + library_dirs: list + Where to find the libraries, ``-L`` passed to the linker + """ + modname = name.split('.')[-1] + dirname = builddir / name + dirname.mkdir(exist_ok=True) + cfile = _convert_str_to_file(source_string, dirname) + include_dirs = [sysconfig.get_config_var('INCLUDEPY')] + include_dirs + + return _c_compile(cfile, outputfilename= dirname / modname, + include_dirs=include_dirs, libraries=[], library_dirs=[], + ) + +def _convert_str_to_file(source, dirname): + """Helper function to create a file ``source.c`` in `dirname` that contains + the string in `source`. Returns the file name + """ + filename = dirname / 'source.c' + with filename.open('w') as f: + f.write(str(source)) + return filename + + +def _make_methods(functions, modname): + """ Turns the name, signature, code in functions into complete functions + and lists them in a methods_table. Then turns the methods_table into a + ``PyMethodDef`` structure and returns the resulting code fragment ready + for compilation + """ + methods_table = [] + codes = [] + for funcname, flags, code in functions: + cfuncname = "%s_%s" % (modname, funcname) + if 'METH_KEYWORDS' in flags: + signature = '(PyObject *self, PyObject *args, PyObject *kwargs)' + else: + signature = '(PyObject *self, PyObject *args)' + methods_table.append( + "{\"%s\", (PyCFunction)%s, %s}," % (funcname, cfuncname, flags)) + func_code = """ + static PyObject* {cfuncname}{signature} + {{ + {code} + }} + """.format(cfuncname=cfuncname, signature=signature, code=code) + codes.append(func_code) + + body = "\n".join(codes) + """ + static PyMethodDef methods[] = { + %(methods)s + { NULL } + }; + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "%(modname)s", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + methods, /* m_methods */ + }; + """ % dict(methods='\n'.join(methods_table), modname=modname) + return body + +def _make_source(name, init, body): + """ Combines the code fragments into source code ready to be compiled + """ + code = """ + #include + + %(body)s + + PyMODINIT_FUNC + PyInit_%(name)s(void) { + %(init)s + } + """ % dict( + name=name, init=init, body=body, + ) + return code + + +def _c_compile(cfile, outputfilename, include_dirs=[], libraries=[], + library_dirs=[]): + if sys.platform == 'win32': + compile_extra = ["/we4013"] + link_extra = ["/LIBPATH:" + os.path.join(sys.exec_prefix, 'libs')] + elif sys.platform.startswith('linux'): + compile_extra = [ + "-O0", "-g", "-Werror=implicit-function-declaration", "-fPIC"] + link_extra = None + else: + compile_extra = link_extra = None + pass + if sys.platform == 'win32': + link_extra = link_extra + ['/DEBUG'] # generate .pdb file + if sys.platform == 'darwin': + # support Fink & Darwinports + for s in ('/sw/', '/opt/local/'): + if (s + 'include' not in include_dirs + and os.path.exists(s + 'include')): + include_dirs.append(s + 'include') + if s + 'lib' not in library_dirs and os.path.exists(s + 'lib'): + library_dirs.append(s + 'lib') + + outputfilename = outputfilename.with_suffix(get_so_suffix()) + saved_environ = os.environ.copy() + try: + build( + cfile, outputfilename, + compile_extra, link_extra, + include_dirs, libraries, library_dirs) + finally: + # workaround for a distutils bugs where some env vars can + # become longer and longer every time it is used + for key, value in saved_environ.items(): + if os.environ.get(key) != value: + os.environ[key] = value + return outputfilename + +def build(cfile, outputfilename, compile_extra, link_extra, + include_dirs, libraries, library_dirs): + "cd into the directory where the cfile is, use distutils to build" + + compiler = new_compiler(force=1) + compiler.verbose = 1 + customize_compiler(compiler) + objects = [] + + old = os.getcwd() + os.chdir(cfile.parent) + try: + res = compiler.compile([str(cfile.name)], + include_dirs=include_dirs, extra_preargs=compile_extra) + objects += [str(cfile.parent / r) for r in res] + finally: + os.chdir(old) + + compiler.link_shared_object( + objects, str(outputfilename), + libraries=libraries, + extra_preargs=link_extra, + library_dirs=library_dirs) + +def get_so_suffix(): + ret = sysconfig.get_config_var('EXT_SUFFIX') + assert ret + return ret + +def get_extbuilder(base_dir): + return ExtensionCompiler( + builddir_base=base_dir, + ) From 5f08532840785890a447f1dc1c832447cbb225ad Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 18 Oct 2020 19:22:55 +0300 Subject: [PATCH 34/76] fix allocation/free exposed by tests --- numpy/core/src/multiarray/getset.c | 3 ++- numpy/core/src/multiarray/methods.c | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index de2a8c14e6aa..c2ced1b305f2 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -384,7 +384,8 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) } if (PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA) { PyArray_XDECREF(self); - PyDataMem_FREE(PyArray_DATA(self)); + PyDataMem_UserFREE(PyArray_DATA(self), PyArray_NBYTES(self), + PyArray_HANDLER(self)->free); } if (PyArray_BASE(self)) { if ((PyArray_FLAGS(self) & NPY_ARRAY_WRITEBACKIFCOPY) || diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index dc23b3471cb2..008d51411eec 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2039,7 +2039,8 @@ array_setstate(PyArrayObject *self, PyObject *args) } if ((PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA)) { - PyDataMem_FREE(PyArray_DATA(self)); + PyDataMem_UserFREE(PyArray_DATA(self), PyArray_NBYTES(self), + PyArray_HANDLER(self)->free); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } Py_XDECREF(PyArray_BASE(self)); @@ -2084,7 +2085,7 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); Py_RETURN_NONE; } - fa->data = PyDataMem_NEW(num); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->alloc); if (PyArray_DATA(self) == NULL) { Py_DECREF(rawdata); return PyErr_NoMemory(); @@ -2132,7 +2133,7 @@ array_setstate(PyArrayObject *self, PyObject *args) if (num == 0 || elsize == 0) { Py_RETURN_NONE; } - fa->data = PyDataMem_NEW(num); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->alloc); if (PyArray_DATA(self) == NULL) { return PyErr_NoMemory(); } From 7266029ccdd6d2e9d4c577b5785259bf3a49b56a Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 18 Oct 2020 22:24:45 +0300 Subject: [PATCH 35/76] DOC: document the new APIs (and some old ones too) --- doc/source/reference/c-api/data_memory.rst | 96 ++++++++++++++++++++++ doc/source/reference/c-api/index.rst | 1 + 2 files changed, 97 insertions(+) create mode 100644 doc/source/reference/c-api/data_memory.rst diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst new file mode 100644 index 000000000000..9023329cbdd0 --- /dev/null +++ b/doc/source/reference/c-api/data_memory.rst @@ -0,0 +1,96 @@ +Memory management +----------------- + +The `numpy.ndarray` is a python class. It requires additinal memory allocations +to hold `numpy.ndarray.strides`, `numpy.ndarray.shape` and +``numpy.ndarray.data`` attributes. These attributes are specially allocated +after creating the python object in `__new__`. The ``strides`` and +``dimensions`` are stored in a piece of memory allocated internally. + +These allocations are small relative to the ``data``, the homogeneous chunk of +memory used to store the actual array values (which could be pointers in the +case of ``object`` arrays). Users may wish to override the internal data +memory routines with ones of their own. They can do this by using +``PyDataMem_SetHandler``, which uses a ``PyDataMem_Handler`` structure to hold +pointers to functions used to manage the data memory. The calls are wrapped +by internal routines to call :c:func:`PyTraceMalloc_Track`, +:c:func:`PyTraceMalloc_Untrack`, and will use the `PyDataMem_EventHookFunc` +mechanism. Since the functions may change during the lifetime of the process, +each `ndarray` carries with it the functions used at the time of its +instantiation, and these will be used to reallocate or free the data memory of +the instance. + +.. c:type:: PyDataMem_Handler + + A struct to hold function pointers used to manipulate memory + + .. code-block:: c + + typedef struct { + char name[128]; /* multiple of 64 to keep the struct unaligned */ + PyDataMem_AllocFunc *alloc; + PyDataMem_ZeroedAllocFunc *zeroed_alloc; + PyDataMem_FreeFunc *free; + PyDataMem_ReallocFunc *realloc; + PyDataMem_CopyFunc *host2obj; /* copy from the host python */ + PyDataMem_CopyFunc *obj2host; /* copy to the host python */ + PyDataMem_CopyFunc *obj2obj; /* copy between two objects */ + } PyDataMem_Handler; + + where the function's signatures are + + .. code-block:: c + + typedef void *(PyDataMem_AllocFunc)(size_t size); + typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); + typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); + typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); + typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); + +.. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) + + Sets a new allocation policy. If the input value is NULL, will reset + the policy to the default. Returns the previous policy, NULL if the + previous policy was the default. We wrap the user-provided functions + so they will still call the python and numpy memory management callback + hooks. + +.. c:function:: const char * PyDataMem_GetHandlerName(PyArrayObject *obj) + + Return the const char name of the PyDataMem_Handler used by the + PyArrayObject. If NULL, return the name of the current global policy that + will be used to allocate data for the next PyArrayObject + +For an example of setting up and using the PyDataMem_Handler, see the test in +``numpy/core/tests/test_mem_policy.py`` + +.. c:function:: typedef void (PyDataMem_EventHookFunc)(void *inp, void *outp, + size_t size, void *user_data); + + This function will be called on NEW,FREE,RENEW calls in data memory + manipulation + + + +.. c:function:: PyDataMem_EventHookFunc * PyDataMem_SetEventHook( + PyDataMem_EventHookFunc *newhook, void *user_data, void **old_data) + + Sets the allocation event hook for numpy array data. + + Returns a pointer to the previous hook or NULL. If old_data is + non-NULL, the previous user_data pointer will be copied to it. + + If not NULL, hook will be called at the end of each PyDataMem_NEW/FREE/RENEW: + + .. code-block:: c + + 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) + + When the hook is called, the GIL will be held by the calling + thread. The hook should be written to be reentrant, if it performs + operations that might cause new allocation events (such as the + creation/destruction numpy objects, or creating/destroying Python + objects which might cause a gc) + diff --git a/doc/source/reference/c-api/index.rst b/doc/source/reference/c-api/index.rst index bb1ed154e9b0..6288ff33bc17 100644 --- a/doc/source/reference/c-api/index.rst +++ b/doc/source/reference/c-api/index.rst @@ -49,3 +49,4 @@ code. generalized-ufuncs coremath deprecations + data_memory From 5d547ff85f118a116a2438d039a49339e708a4e5 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Mon, 19 Oct 2020 16:12:25 +0300 Subject: [PATCH 36/76] BUG: return void from FREE, also some cleanup --- numpy/core/src/multiarray/alloc.c | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 295c248565d7..c123be1529e4 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -195,8 +195,8 @@ npy_free_cache_dim(void * p, npy_uintp sz) /* malloc/free/realloc hook */ -NPY_NO_EXPORT PyDataMem_EventHookFunc *_PyDataMem_eventhook; -NPY_NO_EXPORT void *_PyDataMem_eventhook_user_data; +NPY_NO_EXPORT PyDataMem_EventHookFunc *_PyDataMem_eventhook = NULL; +NPY_NO_EXPORT void *_PyDataMem_eventhook_user_data = NULL; /*NUMPY_API * Sets the allocation event hook for numpy array data. @@ -325,12 +325,6 @@ PyDataMem_RENEW(void *ptr, size_t size) return result; } -typedef void *(alloc_wrapper)(size_t, PyDataMem_AllocFunc *); -typedef void *(zalloc_wrapper)(size_t nelems, size_t elsize); -typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); -typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); -typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); - /* Memory handler global default */ static PyDataMem_Handler default_allocator = { "default_allocator", @@ -401,7 +395,8 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMem_FreeFunc *func) { if (func == npy_free_cache) { // All the logic below is conditionally handled by npy_free_cache - return npy_free_cache(ptr, size); + npy_free_cache(ptr, size); + return; } PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); func(ptr, size); From 4617c50eb8cc58371c0cfde3925445a508d91755 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Mon, 19 Oct 2020 22:47:30 +0300 Subject: [PATCH 37/76] MAINT: changes from review --- doc/source/reference/c-api/data_memory.rst | 60 ++++++++++++++++------ numpy/core/include/numpy/ndarraytypes.h | 2 +- numpy/core/src/multiarray/alloc.c | 2 +- numpy/core/src/multiarray/alloc.h | 1 + numpy/core/src/multiarray/ctors.c | 6 ++- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index 9023329cbdd0..df55305e2188 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -1,24 +1,52 @@ -Memory management ------------------ +Memory management in NumPy +========================== -The `numpy.ndarray` is a python class. It requires additinal memory allocations +The `numpy.ndarray` is a python class. It requires additional memory allocations to hold `numpy.ndarray.strides`, `numpy.ndarray.shape` and -``numpy.ndarray.data`` attributes. These attributes are specially allocated +`numpy.ndarray.data` attributes. These attributes are specially allocated after creating the python object in `__new__`. The ``strides`` and -``dimensions`` are stored in a piece of memory allocated internally. +``shape`` are stored in a piece of memory allocated internally. These allocations are small relative to the ``data``, the homogeneous chunk of memory used to store the actual array values (which could be pointers in the -case of ``object`` arrays). Users may wish to override the internal data -memory routines with ones of their own. They can do this by using -``PyDataMem_SetHandler``, which uses a ``PyDataMem_Handler`` structure to hold -pointers to functions used to manage the data memory. The calls are wrapped -by internal routines to call :c:func:`PyTraceMalloc_Track`, -:c:func:`PyTraceMalloc_Untrack`, and will use the `PyDataMem_EventHookFunc` -mechanism. Since the functions may change during the lifetime of the process, -each `ndarray` carries with it the functions used at the time of its -instantiation, and these will be used to reallocate or free the data memory of -the instance. +case of ``object`` arrays). Since that memory can be significantly large, NumPy +has provided interfaces to manage it. This document details how those +interfaces work. + +Historical overview +------------------- + +Since version 1.7.0, NumPy has exposed a set of ``PyDataMem_*`` functions +(:c:func:`PyDataMem_NEW`, :c:func:`PyDataMem_FREE`, :c:func:`PyDataMem_RENEW`) +which are backed by `alloc`, `free`, `realloc` respectively. In that version +NumPy also exposed the `PyDataMem_EventHook` function described below, which +wrap the OS-level calls. + +Python also improved its memory management capabilities, and began providing +various :ref:`management policies ` beginning in version +3.4. These routines are divided into a set of domains, each domain has a +:c:type:`PyMemAllocatorEx` structure of routines for memory management. Python also +added a `tracemalloc` module to trace calls to the various routines. These +tracking hooks were added to the NumPy ``PyDataMem_*`` routines. + +Configurable memory routines in NumPy +------------------------------------- + +Users may wish to override the internal data memory routines with ones of their +own. Since NumPy does not use the Python domain strategy to manage data memory, +it provides an alternative set of C-APIs to change memory routines. There are +no Python domain-wide strategies for large chunks of object data, so those are +less suited to NumPy's needs. User who wish to change the NumPy data memory +management routines can use :c:func:`PyDataMem_SetHandler`, which uses a +:c:type:`PyDataMem_Handler` structure to hold pointers to functions used to +manage the data memory. The calls are still wrapped by internal routines to +call :c:func:`PyTraceMalloc_Track`, :c:func:`PyTraceMalloc_Untrack`, and will +use the :c:func:`PyDataMem_EventHookFunc` mechanism. Since the functions may +change during the lifetime of the process, each ``ndarray`` carries with it the +functions used at the time of its instantiation, and these will be used to +reallocate or free the data memory of the instance. As of NumPy version 1.20, +the copy functions are not yet implemented, all memory copies are handled by +calls to ``memcpy``. .. c:type:: PyDataMem_Handler @@ -62,7 +90,7 @@ the instance. will be used to allocate data for the next PyArrayObject For an example of setting up and using the PyDataMem_Handler, see the test in -``numpy/core/tests/test_mem_policy.py`` +:file:`numpy/core/tests/test_mem_policy.py` .. c:function:: typedef void (PyDataMem_EventHookFunc)(void *inp, void *outp, size_t size, void *user_data); diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index 31b09c371c78..c91abe4e8a45 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -674,7 +674,7 @@ typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); typedef struct { - char name[128]; /* multiple of 64 to keep the struct unaligned */ + char name[128]; /* multiple of 64 to keep the struct aligned */ PyDataMem_AllocFunc *alloc; PyDataMem_ZeroedAllocFunc *zeroed_alloc; PyDataMem_FreeFunc *free; diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index c123be1529e4..4cf3bb60d768 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -326,7 +326,7 @@ PyDataMem_RENEW(void *ptr, size_t size) } /* Memory handler global default */ -static PyDataMem_Handler default_allocator = { +PyDataMem_Handler default_allocator = { "default_allocator", npy_alloc_cache, /* alloc */ npy_alloc_cache_zero, /* zeroed_alloc */ diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index 5a1726090358..c8aa84947212 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -40,6 +40,7 @@ npy_free_cache_dim_array(PyArrayObject * arr) } extern PyDataMem_Handler *current_allocator; +extern PyDataMem_Handler default_allocator; #define PyArray_HANDLER(arr) ((PyArrayObject_fields*)(arr))->mem_handler diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index f4161bfb0689..b490a4d30f26 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -804,10 +804,10 @@ PyArray_NewFromDescr_int( fa->flags |= NPY_ARRAY_C_CONTIGUOUS|NPY_ARRAY_F_CONTIGUOUS; } - /* Store the functions in case the global hander is modified */ - fa->mem_handler = current_allocator; if (data == NULL) { + /* Store the functions in case the global hander is modified */ + fa->mem_handler = current_allocator; /* * Allocate something even for zero-space arrays * e.g. shape=(0,) -- otherwise buffer exposure @@ -836,6 +836,8 @@ PyArray_NewFromDescr_int( fa->flags |= NPY_ARRAY_OWNDATA; } else { + /* The handlers should never be called in this case, but just in case */ + fa->mem_handler = &default_allocator; /* * If data is passed in, this object won't own it by default. * Caller must arrange for this to be reset if truly desired From 8f739c47ffaebd6009a11eff6ddfef8a05a377d7 Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 11 Mar 2021 21:41:15 +0200 Subject: [PATCH 38/76] fixes from linter --- doc/TESTS.rst.txt | 2 +- doc/source/reference/c-api/data_memory.rst | 2 +- numpy/core/code_generators/cversions.txt | 2 +- numpy/core/code_generators/numpy_api.py | 8 ++--- numpy/core/tests/test_mem_policy.py | 39 ++++++++++----------- numpy/testing/_private/extbuild.py | 40 +++++++++++++--------- tools/lint_diff.ini | 4 ++- 7 files changed, 52 insertions(+), 45 deletions(-) diff --git a/doc/TESTS.rst.txt b/doc/TESTS.rst.txt index 0b9cc610eb1e..06b34138c845 100644 --- a/doc/TESTS.rst.txt +++ b/doc/TESTS.rst.txt @@ -153,7 +153,7 @@ modules written "as-if" they know nothing about the internals of NumPy, rather using the official C-API interfaces only. Examples of such modules are tests for a user-defined ``rational`` dtype in ``_rational_tests`` or the ufunc machinery tests in ``_umath_tests`` which are part of the binary distribution. -Starting from version 1.20, you can also write snippets of C code in tests that +Starting from version 1.21, you can also write snippets of C code in tests that will be compiled locally into c-extension modules and loaded into python. .. currentmodule:: numpy.testing.extbuild diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index df55305e2188..bbeba10aaa4a 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -44,7 +44,7 @@ call :c:func:`PyTraceMalloc_Track`, :c:func:`PyTraceMalloc_Untrack`, and will use the :c:func:`PyDataMem_EventHookFunc` mechanism. Since the functions may change during the lifetime of the process, each ``ndarray`` carries with it the functions used at the time of its instantiation, and these will be used to -reallocate or free the data memory of the instance. As of NumPy version 1.20, +reallocate or free the data memory of the instance. As of NumPy version 1.21, the copy functions are not yet implemented, all memory copies are handled by calls to ``memcpy``. diff --git a/numpy/core/code_generators/cversions.txt b/numpy/core/code_generators/cversions.txt index 4a2e68525612..c632820054dd 100644 --- a/numpy/core/code_generators/cversions.txt +++ b/numpy/core/code_generators/cversions.txt @@ -59,5 +59,5 @@ # Version 14 (NumPy 1.22) No change. 0x0000000e = 17a0f366e55ec05e5c5c149123478452 -# Version 15 (NumPy 1.20) Configurable memory allocations +# Version 15 (NumPy 1.21) Configurable memory allocations 0x0000000f = 4177738910303368a00be8b1ce9283d5 diff --git a/numpy/core/code_generators/numpy_api.py b/numpy/core/code_generators/numpy_api.py index a915bbb35b5f..59ae7c592d25 100644 --- a/numpy/core/code_generators/numpy_api.py +++ b/numpy/core/code_generators/numpy_api.py @@ -76,9 +76,9 @@ # End 1.6 API } -#define NPY_NUMUSERTYPES (*(int *)PyArray_API[6]) -#define PyBoolArrType_Type (*(PyTypeObject *)PyArray_API[7]) -#define _PyArrayScalar_BoolValues ((PyBoolScalarObject *)PyArray_API[8]) +# define NPY_NUMUSERTYPES (*(int *)PyArray_API[6]) +# define PyBoolArrType_Type (*(PyTypeObject *)PyArray_API[7]) +# define _PyArrayScalar_BoolValues ((PyBoolScalarObject *)PyArray_API[8]) multiarray_funcs_api = { 'PyArray_GetNDArrayCVersion': (0,), @@ -352,7 +352,7 @@ # End 1.14 API 'PyDataMem_SetHandler': (304,), 'PyDataMem_GetHandlerName': (305,), - # End 1.20 API + # End 1.21 API } ufunc_types_api = { diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index f6320f66410c..d6e7c03b05ab 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -1,9 +1,8 @@ -import pathlib import pytest -import tempfile import numpy as np from numpy.testing import extbuild + @pytest.fixture def get_module(tmp_path): """ Add a memory policy that returns a false pointer 64 bytes into the @@ -18,21 +17,22 @@ def get_module(tmp_path): PyErr_SetString(PyExc_ValueError, "must be called with a numpy scalar or ndarray"); } - return PyUnicode_FromString(PyDataMem_GetHandlerName((PyArrayObject*)args)); + return PyUnicode_FromString( + PyDataMem_GetHandlerName((PyArrayObject*)args)); """ ), ("set_new_policy", "METH_NOARGS", - """ - const PyDataMem_Handler *old = PyDataMem_SetHandler(&new_handler); - return PyUnicode_FromString(old->name); - """), + """ + const PyDataMem_Handler *old = PyDataMem_SetHandler(&new_handler); + return PyUnicode_FromString(old->name); + """), ("set_old_policy", "METH_NOARGS", - """ - const PyDataMem_Handler *old = PyDataMem_SetHandler(NULL); - return PyUnicode_FromString(old->name); - """), + """ + const PyDataMem_Handler *old = PyDataMem_SetHandler(NULL); + return PyUnicode_FromString(old->name); + """), ] - prologue=''' + prologue = ''' #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include NPY_NO_EXPORT void * @@ -99,20 +99,18 @@ def get_module(tmp_path): memcpy, /* obj2obj */ }; ''' - more_init="import_array();" + more_init = "import_array();" try: import mem_policy return mem_policy except ImportError: pass # if it does not exist, build and load it - try: - return extbuild.build_and_import_extension('mem_policy', - functions, prologue=prologue, include_dirs=[np.get_include()], - build_dir=tmp_path, more_init=more_init) - except: - raise - pytest.skip("could not build module") + return extbuild.build_and_import_extension( + 'mem_policy', + functions, prologue=prologue, include_dirs=[np.get_include()], + build_dir=tmp_path, more_init=more_init + ) def test_set_policy(get_module): @@ -122,6 +120,7 @@ def test_set_policy(get_module): if orig_policy == 'default_allocator': get_module.set_old_policy() + @pytest.mark.slow def test_new_policy(get_module): a = np.arange(10) diff --git a/numpy/testing/_private/extbuild.py b/numpy/testing/_private/extbuild.py index a94ca94c1821..53f819be7cc0 100644 --- a/numpy/testing/_private/extbuild.py +++ b/numpy/testing/_private/extbuild.py @@ -7,17 +7,18 @@ import os import pathlib import sys -import setuptools +import setuptools # noqa import sysconfig from distutils.ccompiler import new_compiler from distutils.sysconfig import customize_compiler from distutils.errors import CompileError -__all__ = ['build_and_import_extensions', 'compile_extension_module'] +__all__ = ['build_and_import_extension', 'compile_extension_module'] -def build_and_import_extension(modname, functions, *, prologue="", build_dir=None, - include_dirs=[], more_init="", - ): + +def build_and_import_extension( + modname, functions, *, prologue="", build_dir=None, + include_dirs=[], more_init=""): """ Build and imports a c-extension module `modname` from a list of function fragments `functions`. @@ -54,7 +55,7 @@ def build_and_import_extension(modname, functions, *, prologue="", build_dir=Non >>> assert not mod.test_bytes(u'abc') >>> assert mod.test_bytes(b'abc') """ - + body = prologue + _make_methods(functions, modname) init = """PyObject *mod = PyModule_Create(&moduledef); """ @@ -78,7 +79,9 @@ def build_and_import_extension(modname, functions, *, prologue="", build_dir=Non spec.loader.exec_module(foo) return foo -def compile_extension_module(name, builddir, include_dirs, + +def compile_extension_module( + name, builddir, include_dirs, source_string, libraries=[], library_dirs=[]): """ Build an extension module and return the filename of the resulting @@ -104,10 +107,12 @@ def compile_extension_module(name, builddir, include_dirs, cfile = _convert_str_to_file(source_string, dirname) include_dirs = [sysconfig.get_config_var('INCLUDEPY')] + include_dirs - return _c_compile(cfile, outputfilename= dirname / modname, + return _c_compile( + cfile, outputfilename=dirname / modname, include_dirs=include_dirs, libraries=[], library_dirs=[], ) + def _convert_str_to_file(source, dirname): """Helper function to create a file ``source.c`` in `dirname` that contains the string in `source`. Returns the file name @@ -157,6 +162,7 @@ def _make_methods(functions, modname): """ % dict(methods='\n'.join(methods_table), modname=modname) return body + def _make_source(name, init, body): """ Combines the code fragments into source code ready to be compiled """ @@ -213,20 +219,24 @@ def _c_compile(cfile, outputfilename, include_dirs=[], libraries=[], os.environ[key] = value return outputfilename + def build(cfile, outputfilename, compile_extra, link_extra, - include_dirs, libraries, library_dirs): + include_dirs, libraries, library_dirs): "cd into the directory where the cfile is, use distutils to build" compiler = new_compiler(force=1) compiler.verbose = 1 customize_compiler(compiler) objects = [] - + old = os.getcwd() os.chdir(cfile.parent) try: - res = compiler.compile([str(cfile.name)], - include_dirs=include_dirs, extra_preargs=compile_extra) + res = compiler.compile( + [str(cfile.name)], + include_dirs=include_dirs, + extra_preargs=compile_extra + ) objects += [str(cfile.parent / r) for r in res] finally: os.chdir(old) @@ -237,12 +247,8 @@ def build(cfile, outputfilename, compile_extra, link_extra, extra_preargs=link_extra, library_dirs=library_dirs) + def get_so_suffix(): ret = sysconfig.get_config_var('EXT_SUFFIX') assert ret return ret - -def get_extbuilder(base_dir): - return ExtensionCompiler( - builddir_base=base_dir, - ) diff --git a/tools/lint_diff.ini b/tools/lint_diff.ini index 3b66d3c3e900..a8f7f21bab8f 100644 --- a/tools/lint_diff.ini +++ b/tools/lint_diff.ini @@ -1,4 +1,6 @@ [pycodestyle] max_line_length = 79 statistics = True -ignore = E121,E122,E123,E125,E126,E127,E128,E226,E251,E265,E266,E302,E402,E704,E712,E721,E731,E741,W291,W293,W391,W503,W504 +ignore = E121,E122,E123,E125,E126,E127,E128,E226,E241,E251,E265,E266,E302,E402,E704,E712,E721,E731,E741,W291,W293,W391,W503,W504 +exclude = numpy/__config__.py,numpy/typing/tests/data +>>>>>>> fixes from linter From 90205b6e6bd41ebf1ff71bc4a2305a43bcaf16f8 Mon Sep 17 00:00:00 2001 From: mattip Date: Fri, 16 Apr 2021 11:17:11 -0400 Subject: [PATCH 39/76] setting ndarray->descr on 0d or scalars mess with FREE --- numpy/core/src/multiarray/arrayobject.c | 11 +++++-- numpy/core/src/multiarray/ctors.c | 1 + numpy/core/src/multiarray/getset.c | 11 ++++++- numpy/core/src/multiarray/methods.c | 19 +++++++++++-- numpy/core/tests/test_mem_policy.py | 38 ++++++++++++++++++------- numpy/ma/mrecords.py | 1 - 6 files changed, 65 insertions(+), 16 deletions(-) diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index b439c11374fd..14d8a0d9b49b 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -493,8 +493,15 @@ array_dealloc(PyArrayObject *self) if (PyDataType_FLAGCHK(fa->descr, NPY_ITEM_REFCOUNT)) { PyArray_XDECREF(self); } - PyDataMem_UserFREE(fa->data, PyArray_NBYTES(self), - fa->mem_handler->free); + /* + * Allocation will never be 0, see comment in ctors.c + * line 820 + */ + size_t nbytes = PyArray_NBYTES(self); + if (nbytes == 0) { + nbytes = fa->descr->elsize ? fa->descr->elsize : 1; + } + PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler->free); } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index b490a4d30f26..92c15872d413 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -813,6 +813,7 @@ PyArray_NewFromDescr_int( * e.g. shape=(0,) -- otherwise buffer exposure * (a.data) doesn't work as it should. * Could probably just allocate a few bytes here. -- Chuck + * Note: always sync this with calls to PyDataMem_UserFREE */ if (nbytes == 0) { nbytes = descr->elsize ? descr->elsize : 1; diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index c2ced1b305f2..c55a0778e399 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -384,7 +384,16 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) } if (PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA) { PyArray_XDECREF(self); - PyDataMem_UserFREE(PyArray_DATA(self), PyArray_NBYTES(self), + size_t nbytes = PyArray_NBYTES(self); + /* + * Allocation will never be 0, see comment in ctors.c + * line 820 + */ + if (nbytes == 0) { + PyArray_Descr *dtype = PyArray_DESCR(self); + nbytes = dtype->elsize ? dtype->elsize : 1; + } + PyDataMem_UserFREE(PyArray_DATA(self), nbytes, PyArray_HANDLER(self)->free); } if (PyArray_BASE(self)) { diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 008d51411eec..7cb23d086860 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -1973,6 +1973,16 @@ array_setstate(PyArrayObject *self, PyObject *args) return NULL; } + /* + * Reassigning fa->descr messes with the reallocation strategy, + * since fa could be a 0-d or scalar, and then + * PyDataMem_UserFREE will be confused + */ + size_t n_tofree = PyArray_NBYTES(self); + if (n_tofree == 0) { + PyArray_Descr *dtype = PyArray_DESCR(self); + n_tofree = dtype->elsize ? dtype->elsize : 1; + } Py_XDECREF(PyArray_DESCR(self)); fa->descr = typecode; Py_INCREF(typecode); @@ -2039,7 +2049,11 @@ array_setstate(PyArrayObject *self, PyObject *args) } if ((PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA)) { - PyDataMem_UserFREE(PyArray_DATA(self), PyArray_NBYTES(self), + /* + * Allocation will never be 0, see comment in ctors.c + * line 820 + */ + PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, PyArray_HANDLER(self)->free); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } @@ -2076,7 +2090,6 @@ array_setstate(PyArrayObject *self, PyObject *args) if (!PyDataType_FLAGCHK(typecode, NPY_LIST_PICKLE)) { int swap = PyArray_ISBYTESWAPPED(self); - fa->data = datastr; /* Bytes should always be considered immutable, but we just grab the * pointer if they are large, to save memory. */ if (!IsAligned(self) || swap || (len <= 1000)) { @@ -2122,7 +2135,9 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); } else { + fa->data = datastr; if (PyArray_SetBaseObject(self, rawdata) < 0) { + Py_DECREF(rawdata); return NULL; } } diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index d6e7c03b05ab..6fa80a4da524 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -41,7 +41,7 @@ def get_module(tmp_path): if (real == NULL) { return NULL; } - snprintf(real, 64, "originally allocated %ld", sz); + snprintf(real, 64, "originally allocated %ld", (unsigned long)sz); return (void *)(real + 64); } NPY_NO_EXPORT void * @@ -50,7 +50,8 @@ def get_module(tmp_path): if (real == NULL) { return NULL; } - snprintf(real, 64, "originally allocated %ld", sz); + snprintf(real, 64, "originally allocated %ld via zero", + (unsigned long)sz); return (void *)(real + 64); } NPY_NO_EXPORT void @@ -60,13 +61,24 @@ def get_module(tmp_path): } char *real = (char *)p - 64; if (strncmp(real, "originally allocated", 20) != 0) { - fprintf(stdout, "uh-oh, unmatched shift_free\\n"); + fprintf(stdout, "uh-oh, unmatched shift_free, " + "no appropriate prefix\\n"); /* Make gcc crash by calling free on the wrong address */ free((char *)p + 10); - /* free(p); */ + /* free(real); */ } else { - free(real); + int i = atoi(real +20); + if (i != sz) { + fprintf(stderr, "uh-oh, unmatched " + "shift_free(ptr, %d) but allocated %d\\n", sz, i); + /* Make gcc crash by calling free on the wrong address */ + /* free((char *)p + 10); */ + free(real); + } + else { + free(real); + } } } NPY_NO_EXPORT void * @@ -84,7 +96,8 @@ def get_module(tmp_path): if (real == NULL) { return NULL; } - snprintf(real, 64, "originally allocated (realloc) %ld", sz); + snprintf(real, 64, "originally allocated " + "%ld via realloc", (unsigned long)sz); return (void *)(real + 64); } } @@ -131,11 +144,16 @@ def test_new_policy(get_module): # test array manipulation. This is slow if orig_policy == 'default_allocator': - # when the test set recurses into this test, the policy will be set - # so this "if" will be false, preventing infinite recursion + # when the np.core.test tests recurse into this test, the + # policy will be set so this "if" will be false, preventing + # infinite recursion # - # if needed, debug this by setting extra_argv=['-vvx'] - np.core.test(verbose=0, extra_argv=[]) + # if needed, debug this by + # - running tests with -- -s (to not capture stdout/stderr + # - setting extra_argv=['-vv'] here + np.core.test(verbose=2, extra_argv=['-vv']) + # also try the ma tests, the pickling test is quite tricky + np.ma.test(verbose=2, extra_argv=['-vv']) get_module.set_old_policy() assert get_module.test_prefix(a) == orig_policy c = np.arange(10) diff --git a/numpy/ma/mrecords.py b/numpy/ma/mrecords.py index 6814931b0b30..c527a7276329 100644 --- a/numpy/ma/mrecords.py +++ b/numpy/ma/mrecords.py @@ -493,7 +493,6 @@ def _mrreconstruct(subtype, baseclass, baseshape, basetype,): _mask = ndarray.__new__(ndarray, baseshape, 'b1') return subtype.__new__(subtype, _data, mask=_mask, dtype=basetype,) - mrecarray = MaskedRecords From 5c0d3f97a6b96886def82704eaacbc02e95c8bef Mon Sep 17 00:00:00 2001 From: mattip Date: Fri, 16 Apr 2021 14:10:58 -0400 Subject: [PATCH 40/76] make scalar allocation more consistent wrt np_alloc_cache --- numpy/core/src/multiarray/scalartypes.c.src | 3 +++ 1 file changed, 3 insertions(+) diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index 2d581845eb98..bb5277f7cfde 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -3032,6 +3032,9 @@ void_arrtype_new(PyTypeObject *type, PyObject *args, PyObject *kwds) (int) NPY_MAX_INT); return NULL; } + if (memu == 0) { + memu = 1; + } destptr = npy_alloc_cache_zero(memu, 1); if (destptr == NULL) { return PyErr_NoMemory(); From 3b385d9d357a8fc35c18652e90c28b327541aa6c Mon Sep 17 00:00:00 2001 From: mattip Date: Fri, 16 Apr 2021 23:31:44 -0400 Subject: [PATCH 41/76] change formatting for sphinx --- doc/source/reference/c-api/data_memory.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index bbeba10aaa4a..bd87f432e75e 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -92,16 +92,14 @@ calls to ``memcpy``. For an example of setting up and using the PyDataMem_Handler, see the test in :file:`numpy/core/tests/test_mem_policy.py` -.. c:function:: typedef void (PyDataMem_EventHookFunc)(void *inp, void *outp, - size_t size, void *user_data); +.. c:function:: void PyDataMem_EventHookFunc(void *inp, void *outp, size_t size, void *user_data); This function will be called on NEW,FREE,RENEW calls in data memory manipulation -.. c:function:: PyDataMem_EventHookFunc * PyDataMem_SetEventHook( - PyDataMem_EventHookFunc *newhook, void *user_data, void **old_data) +.. c:function:: PyDataMem_EventHookFunc * PyDataMem_SetEventHook(PyDataMem_EventHookFunc *newhook, void *user_data, void **old_data) Sets the allocation event hook for numpy array data. From e6e12a3a816609ce9f8dbf1531a8a3a1cfed8ca8 Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 19 Apr 2021 19:21:34 -0400 Subject: [PATCH 42/76] remove memcpy variants --- numpy/core/include/numpy/ndarraytypes.h | 3 --- numpy/core/src/multiarray/alloc.c | 5 +---- numpy/core/tests/test_mem_policy.py | 5 +---- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index c91abe4e8a45..a6da4d26f27b 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -679,9 +679,6 @@ typedef struct { PyDataMem_ZeroedAllocFunc *zeroed_alloc; PyDataMem_FreeFunc *free; PyDataMem_ReallocFunc *realloc; - PyDataMem_CopyFunc *host2obj; /* copy from the host python */ - PyDataMem_CopyFunc *obj2host; /* copy to the host python */ - PyDataMem_CopyFunc *obj2obj; /* copy between two objects */ } PyDataMem_Handler; diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 4cf3bb60d768..7b5d7e0704b5 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -331,10 +331,7 @@ PyDataMem_Handler default_allocator = { npy_alloc_cache, /* alloc */ npy_alloc_cache_zero, /* zeroed_alloc */ npy_free_cache, /* free */ - PyDataMem_RENEW, /* realloc */ - memcpy, /* host2obj */ - memcpy, /* obj2host */ - memcpy, /* obj2obj */ + PyDataMem_RENEW /* realloc */ }; PyDataMem_Handler *current_allocator = &default_allocator; diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 6fa80a4da524..1c6d75b58146 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -106,10 +106,7 @@ def get_module(tmp_path): shift_alloc, /* alloc */ shift_zero, /* zeroed_alloc */ shift_free, /* free */ - shift_realloc, /* realloc */ - memcpy, /* host2obj */ - memcpy, /* obj2host */ - memcpy, /* obj2obj */ + shift_realloc /* realloc */ }; ''' more_init = "import_array();" From 048552d535f462cf6dcbff5d29885de02fe39ee0 Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 3 May 2021 21:18:05 +0300 Subject: [PATCH 43/76] update to match NEP 49 --- doc/source/reference/c-api/data_memory.rst | 52 +++++++++++----------- numpy/core/include/numpy/ndarraytypes.h | 1 - 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index bd87f432e75e..7507c52af8a7 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -7,11 +7,10 @@ to hold `numpy.ndarray.strides`, `numpy.ndarray.shape` and after creating the python object in `__new__`. The ``strides`` and ``shape`` are stored in a piece of memory allocated internally. -These allocations are small relative to the ``data``, the homogeneous chunk of -memory used to store the actual array values (which could be pointers in the -case of ``object`` arrays). Since that memory can be significantly large, NumPy -has provided interfaces to manage it. This document details how those -interfaces work. +The ``data`` allocation used to store the actual array values (which could be +pointers in the case of ``object`` arrays) can be very large, so NumPy has +provided interfaces to manage its allocation and release. This document details +how those interfaces work. Historical overview ------------------- @@ -22,15 +21,23 @@ which are backed by `alloc`, `free`, `realloc` respectively. In that version NumPy also exposed the `PyDataMem_EventHook` function described below, which wrap the OS-level calls. -Python also improved its memory management capabilities, and began providing +Since those early days, Python also improved its memory management +capabilities, and began providing various :ref:`management policies ` beginning in version 3.4. These routines are divided into a set of domains, each domain has a :c:type:`PyMemAllocatorEx` structure of routines for memory management. Python also added a `tracemalloc` module to trace calls to the various routines. These tracking hooks were added to the NumPy ``PyDataMem_*`` routines. -Configurable memory routines in NumPy -------------------------------------- +NumPy added a small cache of allocated memory in its internal +``npy_alloc_cache``, ``npy_alloc_cache_zero``, and ``npy_free_cache`` +functions. These wrap ``alloc``, ``alloc-and-memset(0)`` and ``free`` +respectively, but when ``npy_free_cache`` is called, it adds the pointer to a +short list of available blocks marked by size. These blocks can be re-used by +subsequent calls to ``npy_alloc*``, avoiding memory thrashing. + +Configurable memory routines in NumPy (NEP 49) +---------------------------------------------- Users may wish to override the internal data memory routines with ones of their own. Since NumPy does not use the Python domain strategy to manage data memory, @@ -44,9 +51,7 @@ call :c:func:`PyTraceMalloc_Track`, :c:func:`PyTraceMalloc_Untrack`, and will use the :c:func:`PyDataMem_EventHookFunc` mechanism. Since the functions may change during the lifetime of the process, each ``ndarray`` carries with it the functions used at the time of its instantiation, and these will be used to -reallocate or free the data memory of the instance. As of NumPy version 1.21, -the copy functions are not yet implemented, all memory copies are handled by -calls to ``memcpy``. +reallocate or free the data memory of the instance. .. c:type:: PyDataMem_Handler @@ -55,14 +60,11 @@ calls to ``memcpy``. .. code-block:: c typedef struct { - char name[128]; /* multiple of 64 to keep the struct unaligned */ + char name[128]; /* multiple of 64 to keep the struct aligned */ PyDataMem_AllocFunc *alloc; PyDataMem_ZeroedAllocFunc *zeroed_alloc; PyDataMem_FreeFunc *free; PyDataMem_ReallocFunc *realloc; - PyDataMem_CopyFunc *host2obj; /* copy from the host python */ - PyDataMem_CopyFunc *obj2host; /* copy to the host python */ - PyDataMem_CopyFunc *obj2obj; /* copy between two objects */ } PyDataMem_Handler; where the function's signatures are @@ -73,29 +75,27 @@ calls to ``memcpy``. typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); - typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); .. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) - Sets a new allocation policy. If the input value is NULL, will reset - the policy to the default. Returns the previous policy, NULL if the + Sets a new allocation policy. If the input value is ``NULL``, will reset + the policy to the default. Returns the previous policy, ``NULL`` if the previous policy was the default. We wrap the user-provided functions so they will still call the python and numpy memory management callback hooks. .. c:function:: const char * PyDataMem_GetHandlerName(PyArrayObject *obj) - Return the const char name of the PyDataMem_Handler used by the - PyArrayObject. If NULL, return the name of the current global policy that - will be used to allocate data for the next PyArrayObject + Return the const char name of the `PyDataMem_Handler` used by the + ``PyArrayObject``. If ``NULL``, return the name of the current global policy + that will be used to allocate data for the next ``PyArrayObject``. For an example of setting up and using the PyDataMem_Handler, see the test in :file:`numpy/core/tests/test_mem_policy.py` .. c:function:: void PyDataMem_EventHookFunc(void *inp, void *outp, size_t size, void *user_data); - This function will be called on NEW,FREE,RENEW calls in data memory - manipulation + This function will be called during data memory manipulation @@ -103,10 +103,10 @@ For an example of setting up and using the PyDataMem_Handler, see the test in Sets the allocation event hook for numpy array data. - Returns a pointer to the previous hook or NULL. If old_data is - non-NULL, the previous user_data pointer will be copied to it. + Returns a pointer to the previous hook or ``NULL``. If old_data is + non-``NULL``, the previous user_data pointer will be copied to it. - If not NULL, hook will be called at the end of each PyDataMem_NEW/FREE/RENEW: + If not ``NULL``, hook will be called at the end of each ``PyDataMem_NEW/FREE/RENEW``: .. code-block:: c diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index a6da4d26f27b..b12332556f05 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -671,7 +671,6 @@ typedef void *(PyDataMem_AllocFunc)(size_t size); typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); -typedef void *(PyDataMem_CopyFunc)(void *dst, const void *src, size_t size); typedef struct { char name[128]; /* multiple of 64 to keep the struct aligned */ From ad6f8ad4fcd8a527377d391f3efea53f288504dd Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 6 May 2021 14:29:33 +0300 Subject: [PATCH 44/76] ENH: add a python-level get_handler_name --- numpy/core/multiarray.py | 4 ++-- numpy/core/src/multiarray/alloc.c | 11 +++++++++++ numpy/core/src/multiarray/multiarraymodule.c | 3 +++ numpy/core/tests/test_mem_policy.py | 6 ++++-- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/numpy/core/multiarray.py b/numpy/core/multiarray.py index 154df6f4d7da..351cd3a1bbb5 100644 --- a/numpy/core/multiarray.py +++ b/numpy/core/multiarray.py @@ -31,8 +31,8 @@ 'count_nonzero', 'c_einsum', 'datetime_as_string', 'datetime_data', 'dot', 'dragon4_positional', 'dragon4_scientific', 'dtype', 'empty', 'empty_like', 'error', 'flagsobj', 'flatiter', 'format_longfloat', - 'frombuffer', 'fromfile', 'fromiter', 'fromstring', 'inner', - 'interp', 'interp_complex', 'is_busday', 'lexsort', + 'frombuffer', 'fromfile', 'fromiter', 'fromstring', 'get_handler_name', + 'inner', 'interp', 'interp_complex', 'is_busday', 'lexsort', 'matmul', 'may_share_memory', 'min_scalar_type', 'ndarray', 'nditer', 'nested_iters', 'normalize_axis_index', 'packbits', 'promote_types', 'putmask', 'ravel_multi_index', 'result_type', 'scalar', diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 7b5d7e0704b5..4cb835dac077 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -464,3 +464,14 @@ PyDataMem_GetHandlerName(PyArrayObject *obj) } return PyArray_HANDLER(obj)->name; } + +NPY_NO_EXPORT PyObject * +get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj) +{ + const char * name = PyDataMem_GetHandlerName(obj); + if (name == NULL) { + return NULL; + } + if (! PyCheck + return PyUnicode_FromString(name); +} diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index ea9c10543469..fbe22b3b9512 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -4428,6 +4428,9 @@ static struct PyMethodDef array_module_methods[] = { {"geterrobj", (PyCFunction) ufunc_geterr, METH_VARARGS, NULL}, + {"get_handler_name", + (PyCFunction) get_handler_name, + METH_O, NULL}, {"_add_newdoc_ufunc", (PyCFunction)add_newdoc_ufunc, METH_VARARGS, NULL}, {"_get_sfloat_dtype", diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 1c6d75b58146..c76d37694d96 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -63,7 +63,7 @@ def get_module(tmp_path): if (strncmp(real, "originally allocated", 20) != 0) { fprintf(stdout, "uh-oh, unmatched shift_free, " "no appropriate prefix\\n"); - /* Make gcc crash by calling free on the wrong address */ + /* Make C runtime crash by calling free on the wrong address */ free((char *)p + 10); /* free(real); */ } @@ -72,7 +72,7 @@ def get_module(tmp_path): if (i != sz) { fprintf(stderr, "uh-oh, unmatched " "shift_free(ptr, %d) but allocated %d\\n", sz, i); - /* Make gcc crash by calling free on the wrong address */ + /* Make C runtime crash by calling free on the wrong address */ /* free((char *)p + 10); */ free(real); } @@ -126,6 +126,8 @@ def get_module(tmp_path): def test_set_policy(get_module): a = np.arange(10) orig_policy = get_module.test_prefix(a) + assert orig_policy == np.core.multiarray.get_handler_name() + assert orig_policy == np.core.multiarray.get_handler_name(a) assert get_module.set_new_policy() == orig_policy if orig_policy == 'default_allocator': get_module.set_old_policy() From 50f8b93c2493321c028592992dd82dc0bf0f086e Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 6 May 2021 16:26:26 +0300 Subject: [PATCH 45/76] ENH: add core.multiarray.get_handler_name --- numpy/core/_add_newdocs.py | 9 +++++++++ numpy/core/src/multiarray/alloc.c | 13 ++++++++++--- numpy/core/src/multiarray/alloc.h | 4 ++++ numpy/core/src/multiarray/multiarraymodule.c | 2 +- numpy/core/tests/test_mem_policy.py | 9 ++++----- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py index 06f2a6376fac..d4b3d6712e36 100644 --- a/numpy/core/_add_newdocs.py +++ b/numpy/core/_add_newdocs.py @@ -4688,6 +4688,15 @@ and then throwing away the ufunc. """) +add_newdoc('numpy.core.multiarray', 'get_handler_name', + """ + get_handler_name(a: ndarray) -> str + + Return the name of the memory handler used by `a`. If not provided, return + the name of the current global memory handler that will be used to allocate + data for the next `ndarray`. + """) + add_newdoc('numpy.core.multiarray', '_set_madvise_hugepage', """ _set_madvise_hugepage(enabled: bool) -> bool diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 4cb835dac077..fd1b38e29d41 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -466,12 +466,19 @@ PyDataMem_GetHandlerName(PyArrayObject *obj) } NPY_NO_EXPORT PyObject * -get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj) +get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args) { - const char * name = PyDataMem_GetHandlerName(obj); + PyObject *arr=NULL; + if (!PyArg_ParseTuple(args, "|O:get_handler_name", &arr)) { + return NULL; + } + if (arr != NULL && !PyArray_Check(arr)) { + PyErr_SetString(PyExc_ValueError, "if supplied, argument must be an ndarray"); + return NULL; + } + const char * name = PyDataMem_GetHandlerName((PyArrayObject *)arr); if (name == NULL) { return NULL; } - if (! PyCheck return PyUnicode_FromString(name); } diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index c8aa84947212..c6a955ae4960 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -42,6 +42,10 @@ npy_free_cache_dim_array(PyArrayObject * arr) extern PyDataMem_Handler *current_allocator; extern PyDataMem_Handler default_allocator; +NPY_NO_EXPORT PyObject * +get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj); + + #define PyArray_HANDLER(arr) ((PyArrayObject_fields*)(arr))->mem_handler #endif diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index fbe22b3b9512..8db2666376c2 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -4430,7 +4430,7 @@ static struct PyMethodDef array_module_methods[] = { METH_VARARGS, NULL}, {"get_handler_name", (PyCFunction) get_handler_name, - METH_O, NULL}, + METH_VARARGS, NULL}, {"_add_newdoc_ufunc", (PyCFunction)add_newdoc_ufunc, METH_VARARGS, NULL}, {"_get_sfloat_dtype", diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index c76d37694d96..764dc0900d89 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -68,12 +68,11 @@ def get_module(tmp_path): /* free(real); */ } else { - int i = atoi(real +20); + npy_uintp i = (npy_uintp)atoi(real +20); if (i != sz) { - fprintf(stderr, "uh-oh, unmatched " - "shift_free(ptr, %d) but allocated %d\\n", sz, i); - /* Make C runtime crash by calling free on the wrong address */ - /* free((char *)p + 10); */ + fprintf(stderr, "uh-oh, unmatched shift_free" + "(ptr, %ld) but allocated %ld\\n", sz, i); + /* This happens in some places, only print */ free(real); } else { From c7438f5b80ddf183a5b6d704dc22b876f362b239 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 30 Jun 2021 16:23:33 +0300 Subject: [PATCH 46/76] Allow closure-like definition of the data mem routines --- doc/source/reference/c-api/data_memory.rst | 18 ++++---- numpy/core/include/numpy/ndarraytypes.h | 18 ++++---- numpy/core/src/multiarray/alloc.c | 50 +++++++++++++--------- numpy/core/src/multiarray/alloc.h | 12 +++--- numpy/core/src/multiarray/arrayobject.c | 2 +- numpy/core/src/multiarray/arraytypes.c.src | 10 ++--- numpy/core/src/multiarray/ctors.c | 18 ++++---- numpy/core/src/multiarray/getset.c | 2 +- numpy/core/src/multiarray/item_selection.c | 12 +++--- numpy/core/src/multiarray/methods.c | 6 +-- numpy/core/src/multiarray/shape.c | 2 +- numpy/core/tests/test_mem_policy.py | 47 +++++++++++++------- 12 files changed, 110 insertions(+), 87 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index 7507c52af8a7..ad143bd7d0c0 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -61,20 +61,20 @@ reallocate or free the data memory of the instance. typedef struct { char name[128]; /* multiple of 64 to keep the struct aligned */ - PyDataMem_AllocFunc *alloc; - PyDataMem_ZeroedAllocFunc *zeroed_alloc; - PyDataMem_FreeFunc *free; - PyDataMem_ReallocFunc *realloc; + PyDataMemAllocator allocator; } PyDataMem_Handler; - where the function's signatures are + where the allocator structure is: .. code-block:: c - typedef void *(PyDataMem_AllocFunc)(size_t size); - typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); - typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); - typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); + 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; .. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index b12332556f05..80313685a81c 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -667,17 +667,17 @@ typedef struct _arr_descr { /* * Memory handler structure for array data. */ -typedef void *(PyDataMem_AllocFunc)(size_t size); -typedef void *(PyDataMem_ZeroedAllocFunc)(size_t nelems, size_t elsize); -typedef void (PyDataMem_FreeFunc)(void *ptr, size_t size); -typedef void *(PyDataMem_ReallocFunc)(void *ptr, size_t size); +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; typedef struct { char name[128]; /* multiple of 64 to keep the struct aligned */ - PyDataMem_AllocFunc *alloc; - PyDataMem_ZeroedAllocFunc *zeroed_alloc; - PyDataMem_FreeFunc *free; - PyDataMem_ReallocFunc *realloc; + PyDataMemAllocator allocator; } PyDataMem_Handler; @@ -732,7 +732,7 @@ typedef struct tagPyArrayObject_fields { PyObject *weakreflist; void *_buffer_info; /* private buffer info, tagged to allow warning */ /* - * For alloc/malloc/realloc/free/memcpy per object + * For malloc/calloc/realloc/free per object */ PyDataMem_Handler *mem_handler; } PyArrayObject_fields; diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index fd1b38e29d41..3d5ef9d96fc2 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -326,31 +326,34 @@ PyDataMem_RENEW(void *ptr, size_t size) } /* Memory handler global default */ -PyDataMem_Handler default_allocator = { +PyDataMem_Handler default_handler = { "default_allocator", - npy_alloc_cache, /* alloc */ - npy_alloc_cache_zero, /* zeroed_alloc */ - npy_free_cache, /* free */ - PyDataMem_RENEW /* realloc */ + { + NULL, /* ctx */ + npy_alloc_cache, /* malloc */ + npy_alloc_cache_zero, /* calloc */ + PyDataMem_RENEW, /* realloc */ + npy_free_cache /* free */ + } }; -PyDataMem_Handler *current_allocator = &default_allocator; +PyDataMem_Handler *current_handler = &default_handler; int uo_index=0; /* user_override index */ /* Wrappers for user-assigned PyDataMem_Handlers */ NPY_NO_EXPORT void * -PyDataMem_UserNEW(size_t size, PyDataMem_AllocFunc *alloc) +PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) { void *result; - if (alloc == npy_alloc_cache) { + if (allocator.malloc == npy_alloc_cache) { // All the logic below is conditionally handled by npy_alloc_cache return npy_alloc_cache(size); } assert(size != 0); - result = alloc(size); + result = allocator.malloc(allocator.ctx, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -365,15 +368,15 @@ PyDataMem_UserNEW(size_t size, PyDataMem_AllocFunc *alloc) } NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *zalloc) +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) { void *result; - if (zalloc == npy_alloc_cache_zero) { + if (allocator.calloc == npy_alloc_cache_zero) { // All the logic below is conditionally handled by npy_alloc_cache_zero return npy_alloc_cache_zero(nmemb, size); } - result = zalloc(nmemb, size); + result = allocator.calloc(allocator.ctx, nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -388,15 +391,15 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *z } NPY_NO_EXPORT void -PyDataMem_UserFREE(void *ptr, size_t size, PyDataMem_FreeFunc *func) +PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (func == npy_free_cache) { + if (allocator.free == npy_free_cache) { // All the logic below is conditionally handled by npy_free_cache npy_free_cache(ptr, size); return; } PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); - func(ptr, size); + allocator.free(allocator.ctx, ptr, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -409,12 +412,17 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMem_FreeFunc *func) } NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func) +PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) { + if (allocator.realloc == PyDataMem_RENEW) { + // All the logic below is conditionally handled by PyDataMem_RENEW + return PyDataMem_RENEW(ptr, size); + } + void *result; assert(size != 0); - result = func(ptr, size); + result = allocator.realloc(allocator.ctx, ptr, size); if (result != ptr) { PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); } @@ -441,12 +449,12 @@ PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func) NPY_NO_EXPORT const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) { - const PyDataMem_Handler *old = current_allocator; + const PyDataMem_Handler *old = current_handler; if (handler) { - current_allocator = handler; + current_handler = handler; } else { - current_allocator = &default_allocator; + current_handler = &default_handler; } return old; } @@ -460,7 +468,7 @@ NPY_NO_EXPORT const char * PyDataMem_GetHandlerName(PyArrayObject *obj) { if (obj == NULL) { - return current_allocator->name; + return current_handler->name; } return PyArray_HANDLER(obj)->name; } diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index c6a955ae4960..12a859b44e98 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -10,16 +10,16 @@ NPY_NO_EXPORT PyObject * _set_madvise_hugepage(PyObject *NPY_UNUSED(self), PyObject *enabled_obj); NPY_NO_EXPORT void * -PyDataMem_UserNEW(npy_uintp sz, PyDataMem_AllocFunc *alloc); +PyDataMem_UserNEW(npy_uintp sz, PyDataMemAllocator allocator); NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMem_ZeroedAllocFunc *zalloc); +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator); NPY_NO_EXPORT void -PyDataMem_UserFREE(void * p, npy_uintp sd, PyDataMem_FreeFunc *func); +PyDataMem_UserFREE(void * p, npy_uintp sd, PyDataMemAllocator allocator); NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMem_ReallocFunc *func); +PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator); NPY_NO_EXPORT void * npy_alloc_cache_dim(npy_uintp sz); @@ -39,8 +39,8 @@ npy_free_cache_dim_array(PyArrayObject * arr) npy_free_cache_dim(PyArray_DIMS(arr), PyArray_NDIM(arr)); } -extern PyDataMem_Handler *current_allocator; -extern PyDataMem_Handler default_allocator; +extern PyDataMem_Handler *current_handler; +extern PyDataMem_Handler default_handler; NPY_NO_EXPORT PyObject * get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj); diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index 14d8a0d9b49b..926813cdbf75 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -501,7 +501,7 @@ array_dealloc(PyArrayObject *self) if (nbytes == 0) { nbytes = fa->descr->elsize ? fa->descr->elsize : 1; } - PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler->free); + PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler->allocator); } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 22c62c20d319..9529f2af745d 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3117,7 +3117,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (swap || new->alignment > 1) { if (swap || !npy_is_aligned(nip1, new->alignment)) { /* create buffer and copy */ - nip1 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->alloc); + nip1 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->allocator); if (nip1 == NULL) { goto finish; } @@ -3127,11 +3127,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } if (swap || !npy_is_aligned(nip2, new->alignment)) { /* create buffer and copy */ - nip2 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->alloc); + nip2 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->allocator); if (nip2 == NULL) { if (nip1 != ip1 + offset) { PyDataMem_UserFREE(nip1, new->elsize, - PyArray_HANDLER(ap)->free); + PyArray_HANDLER(ap)->allocator); } goto finish; } @@ -3143,10 +3143,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, dummy); if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { - PyDataMem_UserFREE(nip1, new->elsize, PyArray_HANDLER(ap)->free); + PyDataMem_UserFREE(nip1, new->elsize, PyArray_HANDLER(ap)->allocator); } if (nip2 != ip2 + offset) { - PyDataMem_UserFREE(nip2, new->elsize, PyArray_HANDLER(ap)->free); + PyDataMem_UserFREE(nip2, new->elsize, PyArray_HANDLER(ap)->allocator); } } if (res != 0) { diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 92c15872d413..5df1aa1146f3 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -807,7 +807,7 @@ PyArray_NewFromDescr_int( if (data == NULL) { /* Store the functions in case the global hander is modified */ - fa->mem_handler = current_allocator; + fa->mem_handler = current_handler; /* * Allocate something even for zero-space arrays * e.g. shape=(0,) -- otherwise buffer exposure @@ -824,10 +824,10 @@ PyArray_NewFromDescr_int( */ if (zeroed || PyDataType_FLAGCHK(descr, NPY_NEEDS_INIT)) { data = PyDataMem_UserNEW_ZEROED(nbytes, 1, - fa->mem_handler->zeroed_alloc); + fa->mem_handler->allocator); } else { - data = PyDataMem_UserNEW(nbytes, fa->mem_handler->alloc); + data = PyDataMem_UserNEW(nbytes, fa->mem_handler->allocator); } if (data == NULL) { raise_memory_error(fa->nd, fa->dimensions, descr); @@ -838,7 +838,7 @@ PyArray_NewFromDescr_int( } else { /* The handlers should never be called in this case, but just in case */ - fa->mem_handler = &default_allocator; + fa->mem_handler = &default_handler; /* * If data is passed in, this object won't own it by default. * Caller must arrange for this to be reset if truly desired @@ -3410,7 +3410,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre if (num < 0 && thisbuf == size) { totalbytes += bytes; tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes, - PyArray_HANDLER(r)->realloc); + PyArray_HANDLER(r)->allocator); if (tmp == NULL) { err = 1; break; @@ -3433,7 +3433,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre if (nsize != 0) { tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize, - PyArray_HANDLER(r)->realloc); + PyArray_HANDLER(r)->allocator); if (tmp == NULL) { err = 1; } @@ -3539,7 +3539,7 @@ PyArray_FromFile(FILE *fp, PyArray_Descr *dtype, npy_intp num, char *sep) char *tmp; if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize, - PyArray_HANDLER(ret)->realloc)) == NULL) { + PyArray_HANDLER(ret)->allocator)) == NULL) { Py_DECREF(dtype); Py_DECREF(ret); return PyErr_NoMemory(); @@ -3824,7 +3824,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) elcount = (i >> 1) + (i < 4 ? 4 : 2) + i; if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) { new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes, - PyArray_HANDLER(ret)->realloc); + PyArray_HANDLER(ret)->allocator); } else { new_data = NULL; @@ -3866,7 +3866,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) goto done; } new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize, - PyArray_HANDLER(ret)->realloc); + PyArray_HANDLER(ret)->allocator); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate array memory"); diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index c55a0778e399..32313867a257 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -394,7 +394,7 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) nbytes = dtype->elsize ? dtype->elsize : 1; } PyDataMem_UserFREE(PyArray_DATA(self), nbytes, - PyArray_HANDLER(self)->free); + PyArray_HANDLER(self)->allocator); } if (PyArray_BASE(self)) { if ((PyArray_FLAGS(self) & NPY_ARRAY_WRITEBACKIFCOPY) || diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 1d48916098a9..e4fb952c2067 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -970,7 +970,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, size = it->size; if (needcopy) { - buffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->alloc); + buffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->allocator); if (buffer == NULL) { ret = -1; goto fail; @@ -1054,7 +1054,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - PyDataMem_UserFREE(buffer, N * elsize, PyArray_HANDLER(op)->free); + PyDataMem_UserFREE(buffer, N * elsize, PyArray_HANDLER(op)->allocator); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -1116,7 +1116,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->alloc); + valbuffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->allocator); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1125,7 +1125,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, if (needidxbuffer) { idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), - PyArray_HANDLER(op)->alloc); + PyArray_HANDLER(op)->allocator); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1214,8 +1214,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - PyDataMem_UserFREE(valbuffer, N * elsize, PyArray_HANDLER(op)->free); - PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), PyArray_HANDLER(op)->free); + PyDataMem_UserFREE(valbuffer, N * elsize, PyArray_HANDLER(op)->allocator); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), PyArray_HANDLER(op)->allocator); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 7cb23d086860..a090f746093f 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2054,7 +2054,7 @@ array_setstate(PyArrayObject *self, PyObject *args) * line 820 */ PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, - PyArray_HANDLER(self)->free); + PyArray_HANDLER(self)->allocator); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } Py_XDECREF(PyArray_BASE(self)); @@ -2098,7 +2098,7 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); Py_RETURN_NONE; } - fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->alloc); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { Py_DECREF(rawdata); return PyErr_NoMemory(); @@ -2148,7 +2148,7 @@ array_setstate(PyArrayObject *self, PyObject *args) if (num == 0 || elsize == 0) { Py_RETURN_NONE; } - fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->alloc); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { return PyErr_NoMemory(); } diff --git a/numpy/core/src/multiarray/shape.c b/numpy/core/src/multiarray/shape.c index a67063987c89..c3612ece8d22 100644 --- a/numpy/core/src/multiarray/shape.c +++ b/numpy/core/src/multiarray/shape.c @@ -122,7 +122,7 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, /* Reallocate space if needed - allocating 0 is forbidden */ new_data = PyDataMem_UserRENEW(PyArray_DATA(self), newnbytes == 0 ? elsize : newnbytes, - PyArray_HANDLER(self)->realloc); + PyArray_HANDLER(self)->allocator); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory for array"); diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 764dc0900d89..52c0085b999b 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -35,9 +35,15 @@ def get_module(tmp_path): prologue = ''' #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include + typedef struct { + void *(*malloc)(size_t); + void *(*calloc)(size_t, size_t); + void *(*realloc)(void *, size_t); + void (*free)(void *); + } Allocator; NPY_NO_EXPORT void * - shift_alloc(size_t sz) { - char *real = (char *)malloc(sz + 64); + shift_alloc(Allocator *ctx, size_t sz) { + char *real = (char *)ctx->malloc(sz + 64); if (real == NULL) { return NULL; } @@ -45,8 +51,8 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void * - shift_zero(size_t sz, size_t cnt) { - char *real = (char *)calloc(sz + 64, cnt); + shift_zero(Allocator *ctx, size_t sz, size_t cnt) { + char *real = (char *)ctx->calloc(sz + 64, cnt); if (real == NULL) { return NULL; } @@ -55,7 +61,7 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void - shift_free(void * p, npy_uintp sz) { + shift_free(Allocator *ctx, void * p, npy_uintp sz) { if (p == NULL) { return ; } @@ -64,8 +70,8 @@ def get_module(tmp_path): fprintf(stdout, "uh-oh, unmatched shift_free, " "no appropriate prefix\\n"); /* Make C runtime crash by calling free on the wrong address */ - free((char *)p + 10); - /* free(real); */ + ctx->free((char *)p + 10); + /* ctx->free(real); */ } else { npy_uintp i = (npy_uintp)atoi(real +20); @@ -73,25 +79,25 @@ def get_module(tmp_path): fprintf(stderr, "uh-oh, unmatched shift_free" "(ptr, %ld) but allocated %ld\\n", sz, i); /* This happens in some places, only print */ - free(real); + ctx->free(real); } else { - free(real); + ctx->free(real); } } } NPY_NO_EXPORT void * - shift_realloc(void * p, npy_uintp sz) { + shift_realloc(Allocator *ctx, void * p, npy_uintp sz) { if (p != NULL) { char *real = (char *)p - 64; if (strncmp(real, "originally allocated", 20) != 0) { fprintf(stdout, "uh-oh, unmatched shift_realloc\\n"); return realloc(p, sz); } - return (void *)((char *)realloc(real, sz + 64) + 64); + return (void *)((char *)ctx->realloc(real, sz + 64) + 64); } else { - char *real = (char *)realloc(p, sz + 64); + char *real = (char *)ctx->realloc(p, sz + 64); if (real == NULL) { return NULL; } @@ -100,12 +106,21 @@ def get_module(tmp_path): return (void *)(real + 64); } } + static Allocator new_handler_ctx = { + malloc, + calloc, + realloc, + free + }; static PyDataMem_Handler new_handler = { "secret_data_allocator", - shift_alloc, /* alloc */ - shift_zero, /* zeroed_alloc */ - shift_free, /* free */ - shift_realloc /* realloc */ + { + &new_handler_ctx, + shift_alloc, /* malloc */ + shift_zero, /* calloc */ + shift_realloc, /* realloc */ + shift_free /* free */ + } }; ''' more_init = "import_array();" From f823ba42b78318f4f79e093e21bf2f6b41a421e4 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 30 Jun 2021 16:58:04 +0300 Subject: [PATCH 47/76] Fix incompatible pointer warnings --- numpy/core/src/multiarray/alloc.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 3d5ef9d96fc2..3fe0d3f38f5f 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -329,11 +329,11 @@ PyDataMem_RENEW(void *ptr, size_t size) PyDataMem_Handler default_handler = { "default_allocator", { - NULL, /* ctx */ - npy_alloc_cache, /* malloc */ - npy_alloc_cache_zero, /* calloc */ - PyDataMem_RENEW, /* realloc */ - npy_free_cache /* free */ + NULL, /* ctx */ + (void *(*)(void *, size_t)) npy_alloc_cache, /* malloc */ + (void *(*)(void *, size_t, size_t)) npy_alloc_cache_zero, /* calloc */ + (void *(*)(void *, void *, size_t)) PyDataMem_RENEW, /* realloc */ + (void (*)(void *, void *, size_t)) npy_free_cache /* free */ } }; @@ -348,7 +348,7 @@ PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) { void *result; - if (allocator.malloc == npy_alloc_cache) { + if ((void *) allocator.malloc == (void *) npy_alloc_cache) { // All the logic below is conditionally handled by npy_alloc_cache return npy_alloc_cache(size); } @@ -371,7 +371,7 @@ NPY_NO_EXPORT void * PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) { void *result; - if (allocator.calloc == npy_alloc_cache_zero) { + if ((void *) allocator.calloc == (void *) npy_alloc_cache_zero) { // All the logic below is conditionally handled by npy_alloc_cache_zero return npy_alloc_cache_zero(nmemb, size); } @@ -393,7 +393,7 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator NPY_NO_EXPORT void PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (allocator.free == npy_free_cache) { + if ((void *) allocator.free == (void *) npy_free_cache) { // All the logic below is conditionally handled by npy_free_cache npy_free_cache(ptr, size); return; @@ -414,7 +414,7 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) NPY_NO_EXPORT void * PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (allocator.realloc == PyDataMem_RENEW) { + if ((void *) allocator.realloc == (void *) PyDataMem_RENEW) { // All the logic below is conditionally handled by PyDataMem_RENEW return PyDataMem_RENEW(ptr, size); } From ad131613507c1eeb2621f64ebf523e3686957ac1 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Thu, 1 Jul 2021 11:16:03 +0300 Subject: [PATCH 48/76] Note PyDataMemAllocator and PyMemAllocatorEx differentiation Co-authored-by: Matti Picus --- doc/source/reference/c-api/data_memory.rst | 2 +- numpy/core/include/numpy/ndarraytypes.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index ad143bd7d0c0..e475e3ddb0e6 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -68,6 +68,7 @@ reallocate or free the data memory of the instance. .. code-block:: c + /* The declaration of free differs from PyMemAllocatorEx */ typedef struct { void *ctx; void* (*malloc) (void *ctx, size_t size); @@ -119,4 +120,3 @@ For an example of setting up and using the PyDataMem_Handler, see the test in operations that might cause new allocation events (such as the creation/destruction numpy objects, or creating/destroying Python objects which might cause a gc) - diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index 80313685a81c..a6e4fbe1a186 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -667,6 +667,7 @@ typedef struct _arr_descr { /* * Memory handler structure for array data. */ +/* The declaration of free differs from PyMemAllocatorEx */ typedef struct { void *ctx; void* (*malloc) (void *ctx, size_t size); From 0a08acd248c1ed1d1b8eba6394ba03bd3fd3f4ae Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Thu, 1 Jul 2021 12:02:03 +0300 Subject: [PATCH 49/76] Redefine default allocator handling --- numpy/core/src/multiarray/alloc.c | 26 ++++++++++++++++---------- tools/lint_diff.ini | 1 - 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 3fe0d3f38f5f..8c62a249a971 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -329,11 +329,11 @@ PyDataMem_RENEW(void *ptr, size_t size) PyDataMem_Handler default_handler = { "default_allocator", { - NULL, /* ctx */ - (void *(*)(void *, size_t)) npy_alloc_cache, /* malloc */ - (void *(*)(void *, size_t, size_t)) npy_alloc_cache_zero, /* calloc */ - (void *(*)(void *, void *, size_t)) PyDataMem_RENEW, /* realloc */ - (void (*)(void *, void *, size_t)) npy_free_cache /* free */ + NULL, /* ctx */ + NULL, /* (npy_alloc_cache) malloc */ + NULL, /* (npy_alloc_cache_zero) calloc */ + NULL, /* (PyDataMem_RENEW) realloc */ + NULL /* (npy_free_cache) free */ } }; @@ -341,14 +341,20 @@ PyDataMem_Handler *current_handler = &default_handler; int uo_index=0; /* user_override index */ -/* Wrappers for user-assigned PyDataMem_Handlers */ +/* + * Wrappers for user-assigned PyDataMem_Handlers + * + * The default data mem allocator routines do not make use of a ctx + * and are specially handled since they already integrate + * eventhook and tracemalloc logic. + */ NPY_NO_EXPORT void * PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) { void *result; - if ((void *) allocator.malloc == (void *) npy_alloc_cache) { + if (!allocator.malloc) { // All the logic below is conditionally handled by npy_alloc_cache return npy_alloc_cache(size); } @@ -371,7 +377,7 @@ NPY_NO_EXPORT void * PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) { void *result; - if ((void *) allocator.calloc == (void *) npy_alloc_cache_zero) { + if (!allocator.calloc) { // All the logic below is conditionally handled by npy_alloc_cache_zero return npy_alloc_cache_zero(nmemb, size); } @@ -393,7 +399,7 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator NPY_NO_EXPORT void PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) { - if ((void *) allocator.free == (void *) npy_free_cache) { + if (!allocator.free) { // All the logic below is conditionally handled by npy_free_cache npy_free_cache(ptr, size); return; @@ -414,7 +420,7 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) NPY_NO_EXPORT void * PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) { - if ((void *) allocator.realloc == (void *) PyDataMem_RENEW) { + if (!allocator.realloc) { // All the logic below is conditionally handled by PyDataMem_RENEW return PyDataMem_RENEW(ptr, size); } diff --git a/tools/lint_diff.ini b/tools/lint_diff.ini index a8f7f21bab8f..9e31050b78a4 100644 --- a/tools/lint_diff.ini +++ b/tools/lint_diff.ini @@ -3,4 +3,3 @@ max_line_length = 79 statistics = True ignore = E121,E122,E123,E125,E126,E127,E128,E226,E241,E251,E265,E266,E302,E402,E704,E712,E721,E731,E741,W291,W293,W391,W503,W504 exclude = numpy/__config__.py,numpy/typing/tests/data ->>>>>>> fixes from linter From 1f0301dda9eae031182db72e771ea50a275a7a43 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 5 Jul 2021 12:30:14 +0300 Subject: [PATCH 50/76] Always allocate new arrays using the current_handler --- numpy/core/src/multiarray/arraytypes.c.src | 10 +++++----- numpy/core/src/multiarray/ctors.c | 6 +++--- numpy/core/src/multiarray/item_selection.c | 12 ++++++------ numpy/core/src/multiarray/methods.c | 6 ++++++ 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 9529f2af745d..ac12d74d5f64 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3117,7 +3117,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (swap || new->alignment > 1) { if (swap || !npy_is_aligned(nip1, new->alignment)) { /* create buffer and copy */ - nip1 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->allocator); + nip1 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); if (nip1 == NULL) { goto finish; } @@ -3127,11 +3127,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } if (swap || !npy_is_aligned(nip2, new->alignment)) { /* create buffer and copy */ - nip2 = PyDataMem_UserNEW(new->elsize, PyArray_HANDLER(ap)->allocator); + nip2 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); if (nip2 == NULL) { if (nip1 != ip1 + offset) { PyDataMem_UserFREE(nip1, new->elsize, - PyArray_HANDLER(ap)->allocator); + current_handler->allocator); } goto finish; } @@ -3143,10 +3143,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, dummy); if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { - PyDataMem_UserFREE(nip1, new->elsize, PyArray_HANDLER(ap)->allocator); + PyDataMem_UserFREE(nip1, new->elsize, current_handler->allocator); } if (nip2 != ip2 + offset) { - PyDataMem_UserFREE(nip2, new->elsize, PyArray_HANDLER(ap)->allocator); + PyDataMem_UserFREE(nip2, new->elsize, current_handler->allocator); } } if (res != 0) { diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 5df1aa1146f3..796b0d3b3c30 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -806,7 +806,7 @@ PyArray_NewFromDescr_int( if (data == NULL) { - /* Store the functions in case the global hander is modified */ + /* Store the functions in case the global handler is modified */ fa->mem_handler = current_handler; /* * Allocate something even for zero-space arrays @@ -837,8 +837,8 @@ PyArray_NewFromDescr_int( fa->flags |= NPY_ARRAY_OWNDATA; } else { - /* The handlers should never be called in this case, but just in case */ - fa->mem_handler = &default_handler; + /* The handlers should never be called in this case */ + fa->mem_handler = NULL; /* * If data is passed in, this object won't own it by default. * Caller must arrange for this to be reset if truly desired diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index e4fb952c2067..1c9ff60ee793 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -970,7 +970,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, size = it->size; if (needcopy) { - buffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->allocator); + buffer = PyDataMem_UserNEW(N * elsize, current_handler->allocator); if (buffer == NULL) { ret = -1; goto fail; @@ -1054,7 +1054,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - PyDataMem_UserFREE(buffer, N * elsize, PyArray_HANDLER(op)->allocator); + PyDataMem_UserFREE(buffer, N * elsize, current_handler->allocator); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -1116,7 +1116,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = PyDataMem_UserNEW(N * elsize, PyArray_HANDLER(op)->allocator); + valbuffer = PyDataMem_UserNEW(N * elsize, current_handler->allocator); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1125,7 +1125,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, if (needidxbuffer) { idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), - PyArray_HANDLER(op)->allocator); + current_handler->allocator); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1214,8 +1214,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - PyDataMem_UserFREE(valbuffer, N * elsize, PyArray_HANDLER(op)->allocator); - PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), PyArray_HANDLER(op)->allocator); + PyDataMem_UserFREE(valbuffer, N * elsize, current_handler->allocator); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), current_handler->allocator); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index a090f746093f..87fb82428225 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2098,6 +2098,8 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); Py_RETURN_NONE; } + /* Store the functions in case the global handler is modified */ + fa->mem_handler = current_handler; fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { Py_DECREF(rawdata); @@ -2135,6 +2137,8 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); } else { + /* The handlers should never be called in this case */ + fa->mem_handler = NULL; fa->data = datastr; if (PyArray_SetBaseObject(self, rawdata) < 0) { Py_DECREF(rawdata); @@ -2148,6 +2152,8 @@ array_setstate(PyArrayObject *self, PyObject *args) if (num == 0 || elsize == 0) { Py_RETURN_NONE; } + /* Store the functions in case the global handler is modified */ + fa->mem_handler = current_handler; fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { return PyErr_NoMemory(); From 3d56aa08453388d92950b2e1de280d70a3dda73f Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 5 Jul 2021 13:42:39 +0300 Subject: [PATCH 51/76] Search for the mem_handler name of the data owner --- doc/source/reference/c-api/data_memory.rst | 6 ++++-- numpy/core/src/multiarray/alloc.c | 23 +++++++++++++++++++--- numpy/core/tests/test_mem_policy.py | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index e475e3ddb0e6..2449c75ad37b 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -64,7 +64,7 @@ reallocate or free the data memory of the instance. PyDataMemAllocator allocator; } PyDataMem_Handler; - where the allocator structure is: + where the allocator structure is .. code-block:: c @@ -88,7 +88,9 @@ reallocate or free the data memory of the instance. .. c:function:: const char * PyDataMem_GetHandlerName(PyArrayObject *obj) Return the const char name of the `PyDataMem_Handler` used by the - ``PyArrayObject``. If ``NULL``, return the name of the current global policy + ``PyArrayObject`` or its base. If neither the ``PyArrayObject`` owns its own + data nor its base is a ``PyArrayObject`` which owns its own data return an + empty string. If ``NULL``, return the name of the current global policy that will be used to allocate data for the next ``PyArrayObject``. For an example of setting up and using the PyDataMem_Handler, see the test in diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 8c62a249a971..dec0698c7d5c 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -467,8 +467,10 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) /*NUMPY_API * Return the const char name of the PyDataMem_Handler used by the - * PyArrayObject. If NULL, return the name of the current global policy that - * will be used to allocate data for the next PyArrayObject + * PyArrayObject or its base. If neither the PyArrayObject owns its own data + * nor its base is a PyArrayObject which owns its own data return an empty string. + * If NULL, return the name of the current global policy that + * will be used to allocate data for the next PyArrayObject. */ NPY_NO_EXPORT const char * PyDataMem_GetHandlerName(PyArrayObject *obj) @@ -476,7 +478,19 @@ PyDataMem_GetHandlerName(PyArrayObject *obj) if (obj == NULL) { return current_handler->name; } - return PyArray_HANDLER(obj)->name; + PyDataMem_Handler *handler; + handler = PyArray_HANDLER(obj); + if (handler != NULL) { + return handler->name; + } + PyObject *base = PyArray_BASE(obj); + if (base != NULL && PyArray_Check(base)) { + handler = PyArray_HANDLER((PyArrayObject *) base); + if (handler != NULL) { + return handler->name; + } + } + return ""; } NPY_NO_EXPORT PyObject * @@ -494,5 +508,8 @@ get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args) if (name == NULL) { return NULL; } + else if (strlen(name) == 0) { + Py_RETURN_NONE; + } return PyUnicode_FromString(name); } diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 52c0085b999b..9285ffeab6db 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -152,7 +152,7 @@ def test_new_policy(get_module): a = np.arange(10) orig_policy = get_module.test_prefix(a) assert get_module.set_new_policy() == orig_policy - b = np.arange(10) + b = np.arange(10).reshape((2, 5)) assert get_module.test_prefix(b) == 'secret_data_allocator' # test array manipulation. This is slow From 8ea681839500c7b0c59d9faa7f1441735b454445 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 5 Jul 2021 16:59:24 +0300 Subject: [PATCH 52/76] Sub-comparisons don't need a local mem_handler --- numpy/core/src/multiarray/arraytypes.c.src | 14 +++++++++++--- numpy/core/src/multiarray/item_selection.c | 2 ++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index ac12d74d5f64..572413d4572f 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3109,14 +3109,16 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } /* Set the fields needed by compare or copyswap */ dummy_struct.descr = new; - dummy_struct.mem_handler = PyArray_HANDLER(ap); swap = PyArray_ISBYTESWAPPED(dummy); nip1 = ip1 + offset; nip2 = ip2 + offset; if (swap || new->alignment > 1) { if (swap || !npy_is_aligned(nip1, new->alignment)) { - /* create buffer and copy */ + /* + * create temporary buffer and copy, + * always use the current handler for internal allocations + */ nip1 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); if (nip1 == NULL) { goto finish; @@ -3126,10 +3128,14 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) new->f->copyswap(nip1, NULL, swap, dummy); } if (swap || !npy_is_aligned(nip2, new->alignment)) { - /* create buffer and copy */ + /* + * create temporary buffer and copy, + * always use the current handler for internal allocations + */ nip2 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); if (nip2 == NULL) { if (nip1 != ip1 + offset) { + /* destroy temporary buffer */ PyDataMem_UserFREE(nip1, new->elsize, current_handler->allocator); } @@ -3143,9 +3149,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, dummy); if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { + /* destroy temporary buffer */ PyDataMem_UserFREE(nip1, new->elsize, current_handler->allocator); } if (nip2 != ip2 + offset) { + /* destroy temporary buffer */ PyDataMem_UserFREE(nip2, new->elsize, current_handler->allocator); } } diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 1c9ff60ee793..0548b08cc31d 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -1054,6 +1054,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); + /* cleanup internal buffer */ PyDataMem_UserFREE(buffer, N * elsize, current_handler->allocator); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ @@ -1214,6 +1215,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); + /* cleanup internal buffers */ PyDataMem_UserFREE(valbuffer, N * elsize, current_handler->allocator); PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), current_handler->allocator); if (ret < 0) { From fb2af4dc35f5f54d75f9808cd31bef75bbb8f514 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 14 Jul 2021 15:02:45 +0300 Subject: [PATCH 53/76] Make the default_handler a valid PyDataMem_Handler --- numpy/core/src/multiarray/alloc.c | 86 ++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index dec0698c7d5c..1a937507c6ad 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -325,15 +325,64 @@ PyDataMem_RENEW(void *ptr, size_t size) return result; } +// The default data mem allocator malloc routine does not make use of a ctx. +// It should be called only through PyDataMem_UserNEW +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void * +default_malloc(void *NPY_UNUSED(ctx), size_t size) +{ + return _npy_alloc_cache(size, 1, NBUCKETS, datacache, &malloc); +} + +// The default data mem allocator calloc routine does not make use of a ctx. +// It should be called only through PyDataMem_UserNEW_ZEROED +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void * +default_calloc(void *NPY_UNUSED(ctx), size_t nelem, size_t elsize) +{ + void * p; + size_t sz = nelem * elsize; + NPY_BEGIN_THREADS_DEF; + if (sz < NBUCKETS) { + p = _npy_alloc_cache(sz, 1, NBUCKETS, datacache, &malloc); + if (p) { + memset(p, 0, sz); + } + return p; + } + NPY_BEGIN_THREADS; + p = calloc(nelem, elsize); + NPY_END_THREADS; + return p; +} + +// The default data mem allocator realloc routine does not make use of a ctx. +// It should be called only through PyDataMem_UserRENEW +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void * +default_realloc(void *NPY_UNUSED(ctx), void *ptr, size_t new_size) +{ + return realloc(ptr, new_size); +} + +// The default data mem allocator free routine does not make use of a ctx. +// It should be called only through PyDataMem_UserFREE +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void +default_free(void *NPY_UNUSED(ctx), void *ptr, size_t size) +{ + _npy_free_cache(ptr, size, NBUCKETS, datacache, &free); +} + /* Memory handler global default */ PyDataMem_Handler default_handler = { "default_allocator", { - NULL, /* ctx */ - NULL, /* (npy_alloc_cache) malloc */ - NULL, /* (npy_alloc_cache_zero) calloc */ - NULL, /* (PyDataMem_RENEW) realloc */ - NULL /* (npy_free_cache) free */ + NULL, /* ctx */ + default_malloc, /* malloc */ + default_calloc, /* calloc */ + default_realloc, /* realloc */ + default_free /* free */ } }; @@ -341,23 +390,13 @@ PyDataMem_Handler *current_handler = &default_handler; int uo_index=0; /* user_override index */ -/* - * Wrappers for user-assigned PyDataMem_Handlers - * - * The default data mem allocator routines do not make use of a ctx - * and are specially handled since they already integrate - * eventhook and tracemalloc logic. - */ +/* Wrappers for the default or any user-assigned PyDataMem_Handler */ NPY_NO_EXPORT void * PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) { void *result; - if (!allocator.malloc) { - // All the logic below is conditionally handled by npy_alloc_cache - return npy_alloc_cache(size); - } assert(size != 0); result = allocator.malloc(allocator.ctx, size); if (_PyDataMem_eventhook != NULL) { @@ -377,11 +416,6 @@ NPY_NO_EXPORT void * PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) { void *result; - if (!allocator.calloc) { - // All the logic below is conditionally handled by npy_alloc_cache_zero - return npy_alloc_cache_zero(nmemb, size); - } - result = allocator.calloc(allocator.ctx, nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF @@ -399,11 +433,6 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator NPY_NO_EXPORT void PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (!allocator.free) { - // All the logic below is conditionally handled by npy_free_cache - npy_free_cache(ptr, size); - return; - } PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); allocator.free(allocator.ctx, ptr, size); if (_PyDataMem_eventhook != NULL) { @@ -420,11 +449,6 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) NPY_NO_EXPORT void * PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) { - if (!allocator.realloc) { - // All the logic below is conditionally handled by PyDataMem_RENEW - return PyDataMem_RENEW(ptr, size); - } - void *result; assert(size != 0); From f05a1c6dd54ac57ef8193304b7d1cfd6742666c8 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 14 Jul 2021 15:03:24 +0300 Subject: [PATCH 54/76] Fix PyDataMem_SetHandler description (NEP discussion) --- numpy/core/src/multiarray/alloc.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 1a937507c6ad..7f81effc56bc 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -471,10 +471,9 @@ PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) /*NUMPY_API * Sets a new allocation policy. If the input value is NULL, will reset - * the policy to the default. Returns the previous policy, NULL if the - * previous policy was the default. We wrap the user-provided functions - * so they will still call the python and numpy memory management callback - * hooks. + * the policy to the default. Returns the previous policy. We wrap + * the user-provided functions so they will still call the python + * and numpy memory management callback hooks. */ NPY_NO_EXPORT const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) From 660e0a49d9bc9c6f1138c9228984450895860409 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Wed, 14 Jul 2021 19:50:58 +0300 Subject: [PATCH 55/76] Pass the allocators by reference --- numpy/core/src/multiarray/alloc.c | 16 ++++++++-------- numpy/core/src/multiarray/alloc.h | 8 ++++---- numpy/core/src/multiarray/arrayobject.c | 2 +- numpy/core/src/multiarray/arraytypes.c.src | 10 +++++----- numpy/core/src/multiarray/ctors.c | 14 +++++++------- numpy/core/src/multiarray/getset.c | 2 +- numpy/core/src/multiarray/item_selection.c | 12 ++++++------ numpy/core/src/multiarray/methods.c | 6 +++--- numpy/core/src/multiarray/shape.c | 2 +- 9 files changed, 36 insertions(+), 36 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 7f81effc56bc..461fe959ffec 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -393,12 +393,12 @@ int uo_index=0; /* user_override index */ /* Wrappers for the default or any user-assigned PyDataMem_Handler */ NPY_NO_EXPORT void * -PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) +PyDataMem_UserNEW(size_t size, const PyDataMemAllocator *allocator) { void *result; assert(size != 0); - result = allocator.malloc(allocator.ctx, size); + result = allocator->malloc(allocator->ctx, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -413,10 +413,10 @@ PyDataMem_UserNEW(size_t size, PyDataMemAllocator allocator) } NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator) +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, const PyDataMemAllocator *allocator) { void *result; - result = allocator.calloc(allocator.ctx, nmemb, size); + result = allocator->calloc(allocator->ctx, nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -431,10 +431,10 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator } NPY_NO_EXPORT void -PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) +PyDataMem_UserFREE(void *ptr, size_t size, const PyDataMemAllocator *allocator) { PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); - allocator.free(allocator.ctx, ptr, size); + allocator->free(allocator->ctx, ptr, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -447,12 +447,12 @@ PyDataMem_UserFREE(void *ptr, size_t size, PyDataMemAllocator allocator) } NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator) +PyDataMem_UserRENEW(void *ptr, size_t size, const PyDataMemAllocator *allocator) { void *result; assert(size != 0); - result = allocator.realloc(allocator.ctx, ptr, size); + result = allocator->realloc(allocator->ctx, ptr, size); if (result != ptr) { PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); } diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index 12a859b44e98..cac40d0d9f82 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -10,16 +10,16 @@ NPY_NO_EXPORT PyObject * _set_madvise_hugepage(PyObject *NPY_UNUSED(self), PyObject *enabled_obj); NPY_NO_EXPORT void * -PyDataMem_UserNEW(npy_uintp sz, PyDataMemAllocator allocator); +PyDataMem_UserNEW(npy_uintp sz, const PyDataMemAllocator *allocator); NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyDataMemAllocator allocator); +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, const PyDataMemAllocator *allocator); NPY_NO_EXPORT void -PyDataMem_UserFREE(void * p, npy_uintp sd, PyDataMemAllocator allocator); +PyDataMem_UserFREE(void * p, npy_uintp sd, const PyDataMemAllocator *allocator); NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, PyDataMemAllocator allocator); +PyDataMem_UserRENEW(void *ptr, size_t size, const PyDataMemAllocator *allocator); NPY_NO_EXPORT void * npy_alloc_cache_dim(npy_uintp sz); diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index 926813cdbf75..37fd54611a6a 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -501,7 +501,7 @@ array_dealloc(PyArrayObject *self) if (nbytes == 0) { nbytes = fa->descr->elsize ? fa->descr->elsize : 1; } - PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler->allocator); + PyDataMem_UserFREE(fa->data, nbytes, &fa->mem_handler->allocator); } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 572413d4572f..d758a3caea9d 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3119,7 +3119,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) * create temporary buffer and copy, * always use the current handler for internal allocations */ - nip1 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); + nip1 = PyDataMem_UserNEW(new->elsize, ¤t_handler->allocator); if (nip1 == NULL) { goto finish; } @@ -3132,12 +3132,12 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) * create temporary buffer and copy, * always use the current handler for internal allocations */ - nip2 = PyDataMem_UserNEW(new->elsize, current_handler->allocator); + nip2 = PyDataMem_UserNEW(new->elsize, ¤t_handler->allocator); if (nip2 == NULL) { if (nip1 != ip1 + offset) { /* destroy temporary buffer */ PyDataMem_UserFREE(nip1, new->elsize, - current_handler->allocator); + ¤t_handler->allocator); } goto finish; } @@ -3150,11 +3150,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { /* destroy temporary buffer */ - PyDataMem_UserFREE(nip1, new->elsize, current_handler->allocator); + PyDataMem_UserFREE(nip1, new->elsize, ¤t_handler->allocator); } if (nip2 != ip2 + offset) { /* destroy temporary buffer */ - PyDataMem_UserFREE(nip2, new->elsize, current_handler->allocator); + PyDataMem_UserFREE(nip2, new->elsize, ¤t_handler->allocator); } } if (res != 0) { diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 796b0d3b3c30..59e5fe7123c5 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -824,10 +824,10 @@ PyArray_NewFromDescr_int( */ if (zeroed || PyDataType_FLAGCHK(descr, NPY_NEEDS_INIT)) { data = PyDataMem_UserNEW_ZEROED(nbytes, 1, - fa->mem_handler->allocator); + &fa->mem_handler->allocator); } else { - data = PyDataMem_UserNEW(nbytes, fa->mem_handler->allocator); + data = PyDataMem_UserNEW(nbytes, &fa->mem_handler->allocator); } if (data == NULL) { raise_memory_error(fa->nd, fa->dimensions, descr); @@ -3410,7 +3410,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre if (num < 0 && thisbuf == size) { totalbytes += bytes; tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes, - PyArray_HANDLER(r)->allocator); + &PyArray_HANDLER(r)->allocator); if (tmp == NULL) { err = 1; break; @@ -3433,7 +3433,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre if (nsize != 0) { tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize, - PyArray_HANDLER(r)->allocator); + &PyArray_HANDLER(r)->allocator); if (tmp == NULL) { err = 1; } @@ -3539,7 +3539,7 @@ PyArray_FromFile(FILE *fp, PyArray_Descr *dtype, npy_intp num, char *sep) char *tmp; if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize, - PyArray_HANDLER(ret)->allocator)) == NULL) { + &PyArray_HANDLER(ret)->allocator)) == NULL) { Py_DECREF(dtype); Py_DECREF(ret); return PyErr_NoMemory(); @@ -3824,7 +3824,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) elcount = (i >> 1) + (i < 4 ? 4 : 2) + i; if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) { new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes, - PyArray_HANDLER(ret)->allocator); + &PyArray_HANDLER(ret)->allocator); } else { new_data = NULL; @@ -3866,7 +3866,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) goto done; } new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize, - PyArray_HANDLER(ret)->allocator); + &PyArray_HANDLER(ret)->allocator); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate array memory"); diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index 32313867a257..3d28df10b79a 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -394,7 +394,7 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) nbytes = dtype->elsize ? dtype->elsize : 1; } PyDataMem_UserFREE(PyArray_DATA(self), nbytes, - PyArray_HANDLER(self)->allocator); + &PyArray_HANDLER(self)->allocator); } if (PyArray_BASE(self)) { if ((PyArray_FLAGS(self) & NPY_ARRAY_WRITEBACKIFCOPY) || diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 0548b08cc31d..f254e3fb0ed8 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -970,7 +970,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, size = it->size; if (needcopy) { - buffer = PyDataMem_UserNEW(N * elsize, current_handler->allocator); + buffer = PyDataMem_UserNEW(N * elsize, ¤t_handler->allocator); if (buffer == NULL) { ret = -1; goto fail; @@ -1055,7 +1055,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); /* cleanup internal buffer */ - PyDataMem_UserFREE(buffer, N * elsize, current_handler->allocator); + PyDataMem_UserFREE(buffer, N * elsize, ¤t_handler->allocator); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -1117,7 +1117,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = PyDataMem_UserNEW(N * elsize, current_handler->allocator); + valbuffer = PyDataMem_UserNEW(N * elsize, ¤t_handler->allocator); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1126,7 +1126,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, if (needidxbuffer) { idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), - current_handler->allocator); + ¤t_handler->allocator); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1216,8 +1216,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); /* cleanup internal buffers */ - PyDataMem_UserFREE(valbuffer, N * elsize, current_handler->allocator); - PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), current_handler->allocator); + PyDataMem_UserFREE(valbuffer, N * elsize, ¤t_handler->allocator); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), ¤t_handler->allocator); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 87fb82428225..1a8446e49340 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2054,7 +2054,7 @@ array_setstate(PyArrayObject *self, PyObject *args) * line 820 */ PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, - PyArray_HANDLER(self)->allocator); + &PyArray_HANDLER(self)->allocator); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } Py_XDECREF(PyArray_BASE(self)); @@ -2100,7 +2100,7 @@ array_setstate(PyArrayObject *self, PyObject *args) } /* Store the functions in case the global handler is modified */ fa->mem_handler = current_handler; - fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); + fa->data = PyDataMem_UserNEW(num, &PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { Py_DECREF(rawdata); return PyErr_NoMemory(); @@ -2154,7 +2154,7 @@ array_setstate(PyArrayObject *self, PyObject *args) } /* Store the functions in case the global handler is modified */ fa->mem_handler = current_handler; - fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(fa)->allocator); + fa->data = PyDataMem_UserNEW(num, &PyArray_HANDLER(fa)->allocator); if (PyArray_DATA(self) == NULL) { return PyErr_NoMemory(); } diff --git a/numpy/core/src/multiarray/shape.c b/numpy/core/src/multiarray/shape.c index c3612ece8d22..09cec841aed2 100644 --- a/numpy/core/src/multiarray/shape.c +++ b/numpy/core/src/multiarray/shape.c @@ -122,7 +122,7 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, /* Reallocate space if needed - allocating 0 is forbidden */ new_data = PyDataMem_UserRENEW(PyArray_DATA(self), newnbytes == 0 ? elsize : newnbytes, - PyArray_HANDLER(self)->allocator); + &PyArray_HANDLER(self)->allocator); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory for array"); From a4f8d7134203fe77b21b0a5dcb8e8472994fb972 Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 9 Aug 2021 11:30:33 +0300 Subject: [PATCH 56/76] remove import of setuptools==49.1.3, doesn't work on python3.10 --- numpy/testing/_private/extbuild.py | 1 - 1 file changed, 1 deletion(-) diff --git a/numpy/testing/_private/extbuild.py b/numpy/testing/_private/extbuild.py index 53f819be7cc0..1bae6a3c86ae 100644 --- a/numpy/testing/_private/extbuild.py +++ b/numpy/testing/_private/extbuild.py @@ -7,7 +7,6 @@ import os import pathlib import sys -import setuptools # noqa import sysconfig from distutils.ccompiler import new_compiler from distutils.sysconfig import customize_compiler From d7b1a1ddbe3757b82f9aac4ffedd4fbcefad071b Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 9 Aug 2021 12:06:16 +0300 Subject: [PATCH 57/76] fix function signatures in test --- numpy/core/tests/test_mem_policy.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 9285ffeab6db..356bf75c473c 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -42,7 +42,8 @@ def get_module(tmp_path): void (*free)(void *); } Allocator; NPY_NO_EXPORT void * - shift_alloc(Allocator *ctx, size_t sz) { + shift_alloc(void *_ctx, size_t sz) { + Allocator *ctx = (Allocator*)_ctx; char *real = (char *)ctx->malloc(sz + 64); if (real == NULL) { return NULL; @@ -51,7 +52,8 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void * - shift_zero(Allocator *ctx, size_t sz, size_t cnt) { + shift_zero(void *_ctx, size_t sz, size_t cnt) { + Allocator *ctx = (Allocator*)_ctx; char *real = (char *)ctx->calloc(sz + 64, cnt); if (real == NULL) { return NULL; @@ -61,7 +63,8 @@ def get_module(tmp_path): return (void *)(real + 64); } NPY_NO_EXPORT void - shift_free(Allocator *ctx, void * p, npy_uintp sz) { + shift_free(void *_ctx, void * p, npy_uintp sz) { + Allocator *ctx = (Allocator*)_ctx; if (p == NULL) { return ; } @@ -87,7 +90,8 @@ def get_module(tmp_path): } } NPY_NO_EXPORT void * - shift_realloc(Allocator *ctx, void * p, npy_uintp sz) { + shift_realloc(void *_ctx, void * p, npy_uintp sz) { + Allocator *ctx = (Allocator*)_ctx; if (p != NULL) { char *real = (char *)p - 64; if (strncmp(real, "originally allocated", 20) != 0) { From ab1a0eb06e882a86c1142d08613bb625500f40c4 Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 9 Aug 2021 21:11:59 +0300 Subject: [PATCH 58/76] try to fix cygwin extension building --- numpy/testing/_private/extbuild.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/numpy/testing/_private/extbuild.py b/numpy/testing/_private/extbuild.py index 1bae6a3c86ae..8b3a438dd131 100644 --- a/numpy/testing/_private/extbuild.py +++ b/numpy/testing/_private/extbuild.py @@ -8,8 +8,7 @@ import pathlib import sys import sysconfig -from distutils.ccompiler import new_compiler -from distutils.sysconfig import customize_compiler +from numpy.distutils.ccompiler import new_compiler from distutils.errors import CompileError __all__ = ['build_and_import_extension', 'compile_extension_module'] @@ -223,9 +222,8 @@ def build(cfile, outputfilename, compile_extra, link_extra, include_dirs, libraries, library_dirs): "cd into the directory where the cfile is, use distutils to build" - compiler = new_compiler(force=1) - compiler.verbose = 1 - customize_compiler(compiler) + compiler = new_compiler(force=1, verbose=2) + compiler.customize('') objects = [] old = os.getcwd() From b92e36c36967bdd019cb0fa7a8384bacbb98bde8 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Mon, 9 Aug 2021 23:57:49 +0300 Subject: [PATCH 59/76] YAPF mem_policy test --- numpy/core/tests/test_mem_policy.py | 42 ++++++++++++++++------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index d8b9e61a9c89..245ba6a894b0 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -12,14 +12,13 @@ def get_module(tmp_path): memory manipulation that the prefix exists, to make sure all alloc/realloc/ free/calloc go via the functions here. """ - functions = [( - "set_secret_data_policy", "METH_NOARGS", - """ - PyDataMem_Handler *old = (PyDataMem_Handler *) PyDataMem_SetHandler(&secret_data_handler); - return PyCapsule_New(old, NULL, NULL); + functions = [ + ("set_secret_data_policy", "METH_NOARGS", """ + const PyDataMem_Handler *old = + PyDataMem_SetHandler(&secret_data_handler); + return PyCapsule_New((void *) old, NULL, NULL); """), - ("set_old_policy", "METH_O", - """ + ("set_old_policy", "METH_O", """ PyDataMem_Handler *old = NULL; if (args != NULL && PyCapsule_CheckExact(args)) { old = (PyDataMem_Handler *) PyCapsule_GetPointer(args, NULL); @@ -27,7 +26,7 @@ def get_module(tmp_path): PyDataMem_SetHandler(old); Py_RETURN_NONE; """), - ] + ] prologue = ''' #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include @@ -136,22 +135,23 @@ def get_module(tmp_path): except ImportError: pass # if it does not exist, build and load it - return extbuild.build_and_import_extension( - 'mem_policy', - functions, prologue=prologue, include_dirs=[np.get_include()], - build_dir=tmp_path, more_init=more_init - ) + return extbuild.build_and_import_extension('mem_policy', + functions, + prologue=prologue, + include_dirs=[np.get_include()], + build_dir=tmp_path, + more_init=more_init) def test_set_policy(get_module): orig_policy_name = np.core.multiarray.get_handler_name() - a = np.arange(10).reshape((2, 5)) # a doesn't own its own data + a = np.arange(10).reshape((2, 5)) # a doesn't own its own data assert np.core.multiarray.get_handler_name(a) == orig_policy_name orig_policy = get_module.set_secret_data_policy() - b = np.arange(10).reshape((2, 5)) # b doesn't own its own data + b = np.arange(10).reshape((2, 5)) # b doesn't own its own data assert np.core.multiarray.get_handler_name(b) == 'secret_data_allocator' if orig_policy_name == 'default_allocator': @@ -188,8 +188,10 @@ async def async_test_context_locality(get_module): orig_policy_name = np.core.multiarray.get_handler_name() event = asyncio.Event() - concurrent_task1 = asyncio.create_task(concurrent_context1(get_module, event)) - concurrent_task2 = asyncio.create_task(concurrent_context2(get_module, orig_policy_name, event)) + concurrent_task1 = asyncio.create_task( + concurrent_context1(get_module, event)) + concurrent_task2 = asyncio.create_task( + concurrent_context2(get_module, orig_policy_name, event)) await concurrent_task1 await concurrent_task2 @@ -228,8 +230,10 @@ def test_thread_locality(get_module): orig_policy_name = np.core.multiarray.get_handler_name() event = threading.Event() - concurrent_task1 = threading.Thread(target=concurrent_thread1, args=(get_module, event)) - concurrent_task2 = threading.Thread(target=concurrent_thread2, args=(get_module, event)) + concurrent_task1 = threading.Thread(target=concurrent_thread1, + args=(get_module, event)) + concurrent_task2 = threading.Thread(target=concurrent_thread2, + args=(get_module, event)) concurrent_task1.start() concurrent_task2.start() concurrent_task1.join() From 3eadf2fedfe82d487c056788a96390fb5080d0e1 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Tue, 10 Aug 2021 13:06:54 +0300 Subject: [PATCH 60/76] Less empty lines, more comments (tests) --- numpy/core/tests/test_mem_policy.py | 61 +++++++++++++---------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 245ba6a894b0..b8be225c4296 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -43,7 +43,7 @@ def get_module(tmp_path): } SecretDataAllocatorFuncs; NPY_NO_EXPORT void * shift_alloc(void *ctx, size_t sz) { - SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *) ctx; + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx; char *real = (char *)funcs->malloc(sz + 64); if (real == NULL) { return NULL; @@ -53,7 +53,7 @@ def get_module(tmp_path): } NPY_NO_EXPORT void * shift_zero(void *ctx, size_t sz, size_t cnt) { - SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *) ctx; + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx; char *real = (char *)funcs->calloc(sz + 64, cnt); if (real == NULL) { return NULL; @@ -64,7 +64,7 @@ def get_module(tmp_path): } NPY_NO_EXPORT void shift_free(void *ctx, void * p, npy_uintp sz) { - SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *) ctx; + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx; if (p == NULL) { return ; } @@ -91,7 +91,7 @@ def get_module(tmp_path): } NPY_NO_EXPORT void * shift_realloc(void *ctx, void * p, npy_uintp sz) { - SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *) ctx; + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx; if (p != NULL) { char *real = (char *)p - 64; if (strncmp(real, "originally allocated", 20) != 0) { @@ -155,81 +155,75 @@ def test_set_policy(get_module): assert np.core.multiarray.get_handler_name(b) == 'secret_data_allocator' if orig_policy_name == 'default_allocator': - get_module.set_old_policy(None) - + get_module.set_old_policy(None) # tests PyDataMem_SetHandler(NULL) assert np.core.multiarray.get_handler_name() == 'default_allocator' else: get_module.set_old_policy(orig_policy) - assert np.core.multiarray.get_handler_name() == orig_policy_name -async def concurrent_context1(get_module, event): - get_module.set_secret_data_policy() - - assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' - +async def concurrent_context1(get_module, orig_policy_name, event): + if orig_policy_name == 'default_allocator': + get_module.set_secret_data_policy() + assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' + else: + get_module.set_old_policy(None) + assert np.core.multiarray.get_handler_name() == 'default_allocator' event.set() async def concurrent_context2(get_module, orig_policy_name, event): await event.wait() - + # the policy is not affected by changes in parallel contexts assert np.core.multiarray.get_handler_name() == orig_policy_name - - -async def secret_data_context(get_module): - assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' - - get_module.set_old_policy(None) + # change policy in the child context + if orig_policy_name == 'default_allocator': + get_module.set_secret_data_policy() + assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' + else: + get_module.set_old_policy(None) + assert np.core.multiarray.get_handler_name() == 'default_allocator' async def async_test_context_locality(get_module): orig_policy_name = np.core.multiarray.get_handler_name() event = asyncio.Event() + # the child contexts inherit the parent policy concurrent_task1 = asyncio.create_task( - concurrent_context1(get_module, event)) + concurrent_context1(get_module, orig_policy_name, event)) concurrent_task2 = asyncio.create_task( concurrent_context2(get_module, orig_policy_name, event)) await concurrent_task1 await concurrent_task2 + # the parent context is not affected by child policy changes assert np.core.multiarray.get_handler_name() == orig_policy_name - orig_policy = get_module.set_secret_data_policy() - - await asyncio.create_task(secret_data_context(get_module)) - - assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' - - get_module.set_old_policy(orig_policy) - def test_context_locality(get_module): asyncio.run(async_test_context_locality(get_module)) def concurrent_thread1(get_module, event): - assert np.core.multiarray.get_handler_name() == 'default_allocator' - get_module.set_secret_data_policy() - assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' - event.set() def concurrent_thread2(get_module, event): event.wait() - + # the policy is not affected by changes in parallel threads assert np.core.multiarray.get_handler_name() == 'default_allocator' + # change policy in the child thread + get_module.set_secret_data_policy() def test_thread_locality(get_module): orig_policy_name = np.core.multiarray.get_handler_name() event = threading.Event() + # the child threads do not inherit the parent policy concurrent_task1 = threading.Thread(target=concurrent_thread1, args=(get_module, event)) concurrent_task2 = threading.Thread(target=concurrent_thread2, @@ -239,6 +233,7 @@ def test_thread_locality(get_module): concurrent_task1.join() concurrent_task2.join() + # the parent thread is not affected by child policy changes assert np.core.multiarray.get_handler_name() == orig_policy_name From dbe9d732fb2d4d8a9f61e28f06c42afe40729339 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Tue, 10 Aug 2021 15:18:55 +0300 Subject: [PATCH 61/76] Apply suggestions from code review (set an exception and) Co-authored-by: Matti Picus --- doc/source/reference/c-api/data_memory.rst | 4 ++-- numpy/core/src/multiarray/alloc.c | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index f7ea8fde9a5f..7c6efa409017 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -80,7 +80,7 @@ reallocate or free the data memory of the instance. .. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) Set a new allocation policy. If the input value is ``NULL``, will reset the - policy to the default. Return the previous policy, or set an exception and + policy to the default. Return the previous policy, or return ``NULL`` if an error has occurred. We wrap the user-provided functions so they will still call the python and numpy memory management callback hooks. @@ -89,7 +89,7 @@ reallocate or free the data memory of the instance. Return the `PyDataMem_Handler` used by the ``PyArrayObject``. If ``NULL``, return the current global policy that will be used to allocate data for the - next ``PyArrayObject``. On failure, set an exception and return ``NULL``. + next ``PyArrayObject``. On failure, return ``NULL``. For an example of setting up and using the PyDataMem_Handler, see the test in :file:`numpy/core/tests/test_mem_policy.py` diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index fe8bae469ccb..62fd6ba793a5 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -471,8 +471,8 @@ PyDataMem_UserRENEW(void *ptr, size_t size, const PyDataMemAllocator *allocator) /*NUMPY_API * Set a new allocation policy. If the input value is NULL, will reset - * the policy to the default. Return the previous policy, or set an exception - * and return NULL if an error has occurred. We wrap the user-provided + * the policy to the default. Return the previous policy, or + * return NULL if an error has occurred. We wrap the user-provided * functions so they will still call the python and numpy * memory management callback hooks. */ @@ -515,7 +515,7 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) /*NUMPY_API * Return the PyDataMem_Handler used by the PyArrayObject. If NULL, return * the current global policy that will be used to allocate data - * for the next PyArrayObject. On failure, set an exception and return NULL. + * for the next PyArrayObject. On failure, return NULL. */ NPY_NO_EXPORT const PyDataMem_Handler * PyDataMem_GetHandler(PyArrayObject *obj) From 0511820d5fae690e249f3a6479aa5f78934e03ab Mon Sep 17 00:00:00 2001 From: mattip Date: Wed, 11 Aug 2021 13:22:14 +0300 Subject: [PATCH 62/76] skip test on cygwin --- numpy/core/tests/test_mem_policy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index b8be225c4296..26a2a5fab9d4 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -3,6 +3,7 @@ import numpy as np import threading from numpy.testing import extbuild +import sys @pytest.fixture @@ -12,6 +13,8 @@ def get_module(tmp_path): memory manipulation that the prefix exists, to make sure all alloc/realloc/ free/calloc go via the functions here. """ + if sys.platform.startswith('cygwin'): + pytest.skip('link fails on cygwin') functions = [ ("set_secret_data_policy", "METH_NOARGS", """ const PyDataMem_Handler *old = From 23c4bc0f124bd325c09ce3f727823a23ddb4f06e Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 13 Aug 2021 07:44:13 +0300 Subject: [PATCH 63/76] update API hash for changed signature --- numpy/core/code_generators/cversions.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/numpy/core/code_generators/cversions.txt b/numpy/core/code_generators/cversions.txt index c632820054dd..38ee4dac2f6f 100644 --- a/numpy/core/code_generators/cversions.txt +++ b/numpy/core/code_generators/cversions.txt @@ -56,8 +56,7 @@ # DType related API additions. # A new field was added to the end of PyArrayObject_fields. # Version 14 (NumPy 1.21) No change. -# Version 14 (NumPy 1.22) No change. 0x0000000e = 17a0f366e55ec05e5c5c149123478452 -# Version 15 (NumPy 1.21) Configurable memory allocations -0x0000000f = 4177738910303368a00be8b1ce9283d5 +# Version 15 (NumPy 1.22) Configurable memory allocations +0x0000000f = 0c420aed67010594eb81f23ddfb02a88 From ed8649bdcb6df254ad4eb7cc861ae0cc11cb13c4 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 13 Aug 2021 07:57:06 +0300 Subject: [PATCH 64/76] TST: add gc.collect to make sure cycles are broken --- numpy/core/tests/test_nditer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/numpy/core/tests/test_nditer.py b/numpy/core/tests/test_nditer.py index 6b743ab27301..73deb2357d6c 100644 --- a/numpy/core/tests/test_nditer.py +++ b/numpy/core/tests/test_nditer.py @@ -9,7 +9,7 @@ from numpy import array, arange, nditer, all from numpy.testing import ( assert_, assert_equal, assert_array_equal, assert_raises, - HAS_REFCOUNT, suppress_warnings + HAS_REFCOUNT, suppress_warnings, break_cycles ) @@ -3148,6 +3148,7 @@ def test_partial_iteration_cleanup(in_dtype, buf_dtype, steps): # Note that resetting does not free references del it + break_cycles() assert count == sys.getrefcount(value) # Repeat the test with `iternext` @@ -3157,6 +3158,7 @@ def test_partial_iteration_cleanup(in_dtype, buf_dtype, steps): it.iternext() del it # should ensure cleanup + break_cycles() assert count == sys.getrefcount(value) From 9aacefa0532949b7b05b881bee2c109a903fd921 Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Thu, 12 Aug 2021 22:47:21 +0300 Subject: [PATCH 65/76] Implement thread-locality for PyPy Co-authored-by: Sebastian Berg --- numpy/core/src/multiarray/alloc.c | 60 +++++++++++++++++++- numpy/core/src/multiarray/alloc.h | 2 + numpy/core/src/multiarray/multiarraymodule.c | 2 + numpy/core/tests/test_mem_policy.py | 3 + 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 62fd6ba793a5..b6cd4e91be07 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -386,7 +386,9 @@ PyDataMem_Handler default_handler = { } }; +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) PyObject *current_handler; +#endif int uo_index=0; /* user_override index */ @@ -482,6 +484,7 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) PyObject *capsule; PyObject *old_capsule; PyDataMem_Handler *old_handler; +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) PyObject *token; if (PyContextVar_Get(current_handler, NULL, &old_capsule)) { return NULL; @@ -510,6 +513,42 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) } Py_DECREF(token); return old_handler; +#else + PyObject *p; + p = PyThreadState_GetDict(); + if (p == NULL) { + return NULL; + } + old_capsule = PyDict_GetItemString(p, "current_allocator"); + if (old_capsule == NULL) { + old_handler = &default_handler; + } + else { + old_handler = (PyDataMem_Handler *) PyCapsule_GetPointer(old_capsule, NULL); + Py_DECREF(old_capsule); + if (old_handler == NULL) { + return NULL; + } + } + if (handler != NULL) { + capsule = PyCapsule_New(handler, NULL, NULL); + if (capsule == NULL) { + return NULL; + } + } + else { + capsule = PyCapsule_New(&default_handler, NULL, NULL); + if (capsule == NULL) { + return NULL; + } + } + const int error = PyDict_SetItemString(p, "current_allocator", capsule); + Py_DECREF(capsule); + if (error) { + return NULL; + } + return old_handler; +#endif } /*NUMPY_API @@ -523,6 +562,7 @@ PyDataMem_GetHandler(PyArrayObject *obj) PyObject *base; PyObject *capsule; PyDataMem_Handler *handler; +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) if (obj == NULL) { if (PyContextVar_Get(current_handler, NULL, &capsule)) { return NULL; @@ -531,7 +571,25 @@ PyDataMem_GetHandler(PyArrayObject *obj) Py_DECREF(capsule); return handler; } - /* If there's a handler, the array owns its own datay */ +#else + PyObject *p; + if (obj == NULL) { + p = PyThreadState_GetDict(); + if (p == NULL) { + return NULL; + } + capsule = PyDict_GetItemString(p, "current_allocator"); + if (capsule == NULL) { + handler = &default_handler; + } + else { + handler = (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); + Py_DECREF(capsule); + } + return handler; + } +#endif + /* If there's a handler, the array owns its own data */ handler = PyArray_HANDLER(obj); if (handler == NULL) { /* diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index 9550ee2d5071..cbe042e7643d 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -39,8 +39,10 @@ npy_free_cache_dim_array(PyArrayObject * arr) npy_free_cache_dim(PyArray_DIMS(arr), PyArray_NDIM(arr)); } +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) extern PyObject *current_handler; /* PyContextVar/PyCapsule */ extern PyDataMem_Handler default_handler; +#endif NPY_NO_EXPORT PyObject * get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj); diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index 629c3fb2f929..23bf178d6fd3 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -4908,6 +4908,7 @@ PyMODINIT_FUNC PyInit__multiarray_umath(void) { if (initumath(m) != 0) { goto err; } +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) /* * Initialize the context-local PyDataMem_Handler capsule. */ @@ -4920,6 +4921,7 @@ PyMODINIT_FUNC PyInit__multiarray_umath(void) { if (current_handler == NULL) { goto err; } +#endif return m; err: diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 26a2a5fab9d4..9e0a70da6cc7 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -205,6 +205,9 @@ async def async_test_context_locality(get_module): def test_context_locality(get_module): + if sys.implementation.name == 'pypy' and sys.pypy_version_info[:3] < (7, 3, + 6): + pytest.skip('no context-locality support in PyPy < 7.3.6') asyncio.run(async_test_context_locality(get_module)) From 79712faffb0cc55a6d7fb76dc95b3097c11dabcc Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Wed, 25 Aug 2021 08:45:08 +0300 Subject: [PATCH 66/76] Update numpy/core/tests/test_mem_policy.py Co-authored-by: Sebastian Berg --- numpy/core/tests/test_mem_policy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 9e0a70da6cc7..097d563cb057 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -44,6 +44,7 @@ def get_module(tmp_path): void *(*realloc)(void *, size_t); void (*free)(void *); } SecretDataAllocatorFuncs; + NPY_NO_EXPORT void * shift_alloc(void *ctx, size_t sz) { SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx; @@ -205,8 +206,8 @@ async def async_test_context_locality(get_module): def test_context_locality(get_module): - if sys.implementation.name == 'pypy' and sys.pypy_version_info[:3] < (7, 3, - 6): + if (sys.implementation.name == 'pypy' + and sys.pypy_version_info[:3] < (7, 3, 6)): pytest.skip('no context-locality support in PyPy < 7.3.6') asyncio.run(async_test_context_locality(get_module)) From a2ae4c0ce0ca73b0855140831237ed518958a911 Mon Sep 17 00:00:00 2001 From: mattip Date: Wed, 25 Aug 2021 10:46:58 +0300 Subject: [PATCH 67/76] fixes from review --- numpy/core/src/multiarray/alloc.c | 14 +++++++------- numpy/core/src/multiarray/ctors.c | 2 +- numpy/core/src/multiarray/methods.c | 4 ++-- numpy/core/tests/test_mem_policy.py | 13 +++++++++++++ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index b6cd4e91be07..37eb6e8c39c5 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -559,7 +559,6 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) NPY_NO_EXPORT const PyDataMem_Handler * PyDataMem_GetHandler(PyArrayObject *obj) { - PyObject *base; PyObject *capsule; PyDataMem_Handler *handler; #if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) @@ -589,17 +588,18 @@ PyDataMem_GetHandler(PyArrayObject *obj) return handler; } #endif - /* If there's a handler, the array owns its own data */ + /* Try to find a handler */ + PyArrayObject *base = obj; handler = PyArray_HANDLER(obj); - if (handler == NULL) { + while (handler == NULL) { + base = (PyArrayObject*)PyArray_BASE(base); /* * If the base is an array which owns its own data, return its allocator. */ - base = PyArray_BASE(obj); - if (base != NULL && PyArray_Check(base) && - PyArray_CHKFLAGS((PyArrayObject *) base, NPY_ARRAY_OWNDATA)) { - return PyArray_HANDLER(base); + if (base == NULL || ! PyArray_Check(base)) { + break; } + handler = PyArray_HANDLER(base); } return handler; } diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index c540abfbeae2..4cc46bb13f8f 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -806,7 +806,7 @@ PyArray_NewFromDescr_int( if (data == NULL) { - /* Store the functions in case the global handler is modified */ + /* Store the handler in case the default is modified */ fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(NULL); if (fa->mem_handler == NULL) { goto fail; diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index cb4348ddb5e7..aee03729b4a2 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2098,7 +2098,7 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); Py_RETURN_NONE; } - /* Store the functions in case the global handler is modified */ + /* Store the handler in case the default is modified */ fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(NULL); if (fa->mem_handler == NULL) { Py_DECREF(rawdata); @@ -2156,7 +2156,7 @@ array_setstate(PyArrayObject *self, PyObject *args) if (num == 0 || elsize == 0) { Py_RETURN_NONE; } - /* Store the functions in case the global handler is modified */ + /* Store the functions in case the default handler is modified */ fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(NULL); if (fa->mem_handler == NULL) { return NULL; diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 097d563cb057..97c070a09b5c 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -166,6 +166,19 @@ def test_set_policy(get_module): assert np.core.multiarray.get_handler_name() == orig_policy_name +def test_policy_propagation(get_module): + + class MyArr(np.ndarray): + pass + + orig_policy_name = np.core.multiarray.get_handler_name() + a = np.arange(10).view(MyArr).reshape((2, 5)) # a doesn't own its own data + assert np.core.multiarray.get_handler_name(a) == orig_policy_name + + b = np.arange(10).view(MyArr).reshape((2, 5)) # b doesn't own its own data + assert np.core.multiarray.get_handler_name(b) == 'secret_data_allocator' + + async def concurrent_context1(get_module, orig_policy_name, event): if orig_policy_name == 'default_allocator': get_module.set_secret_data_policy() From 09b9c0d88ae73c1cf48bd5d866adcdcf4c017e1b Mon Sep 17 00:00:00 2001 From: mattip Date: Wed, 25 Aug 2021 11:57:47 +0300 Subject: [PATCH 68/76] update circleci config --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8cf18d809d86..fdb85be98be6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: docker: # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ - - image: circleci/python:3.8.4 + - image: cimg/python:3.8 working_directory: ~/repo @@ -23,7 +23,7 @@ jobs: name: create virtual environment, install dependencies command: | sudo apt-get update - sudo apt-get install -y graphviz texlive-fonts-recommended texlive-latex-recommended texlive-latex-extra texlive-generic-extra latexmk texlive-xetex + sudo apt-get install -y graphviz texlive-fonts-recommended texlive-latex-recommended texlive-latex-extra latexmk texlive-xetex python3.8 -m venv venv . venv/bin/activate From efb3c77fe9c176f6183e8c16e12cc5671057b915 Mon Sep 17 00:00:00 2001 From: mattip Date: Wed, 25 Aug 2021 12:04:23 +0300 Subject: [PATCH 69/76] fix test --- numpy/core/tests/test_mem_policy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 97c070a09b5c..2361598e3d23 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -175,8 +175,11 @@ class MyArr(np.ndarray): a = np.arange(10).view(MyArr).reshape((2, 5)) # a doesn't own its own data assert np.core.multiarray.get_handler_name(a) == orig_policy_name + orig_policy = get_module.set_secret_data_policy() + secret_policy_name = np.core.multiarray.get_handler_name() b = np.arange(10).view(MyArr).reshape((2, 5)) # b doesn't own its own data - assert np.core.multiarray.get_handler_name(b) == 'secret_data_allocator' + assert np.core.multiarray.get_handler_name(b) == secret_policy_name + get_module.set_old_policy(orig_policy) async def concurrent_context1(get_module, orig_policy_name, event): From 2945c644d20ee2d5abd0e4062d181ee6bd6058e6 Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 26 Aug 2021 00:09:03 +0300 Subject: [PATCH 70/76] make the connection between OWNDATA and having a allocator handle more explicit --- numpy/core/_add_newdocs.py | 5 +++-- numpy/core/src/multiarray/alloc.c | 25 ++++++++----------------- numpy/core/src/multiarray/ctors.c | 8 ++++++-- numpy/core/src/multiarray/getset.c | 10 ++++++++-- numpy/core/src/multiarray/methods.c | 10 ++++++++-- numpy/core/src/multiarray/shape.c | 9 ++++++++- numpy/core/tests/test_mem_policy.py | 21 ++++++++++++--------- numpy/core/tests/test_nditer.py | 2 ++ 8 files changed, 55 insertions(+), 35 deletions(-) diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py index d4b3d6712e36..6da695d23ac2 100644 --- a/numpy/core/_add_newdocs.py +++ b/numpy/core/_add_newdocs.py @@ -4690,11 +4690,12 @@ add_newdoc('numpy.core.multiarray', 'get_handler_name', """ - get_handler_name(a: ndarray) -> str + get_handler_name(a: ndarray) -> str,None Return the name of the memory handler used by `a`. If not provided, return the name of the current global memory handler that will be used to allocate - data for the next `ndarray`. + data for the next `ndarray`. May return None if `a` does not own its + memory, in which case you can traverse ``a.base`` for a memory handler. """) add_newdoc('numpy.core.multiarray', '_set_madvise_hugepage', diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 37eb6e8c39c5..309bf020740b 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -552,9 +552,10 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) } /*NUMPY_API - * Return the PyDataMem_Handler used by the PyArrayObject. If NULL, return + * Return the PyDataMem_Handler used by obj. If obj is NULL, return * the current global policy that will be used to allocate data - * for the next PyArrayObject. On failure, return NULL. + * for the next PyArrayObject. On failure, return NULL. Can also return NULL + * if obj does not own its memory */ NPY_NO_EXPORT const PyDataMem_Handler * PyDataMem_GetHandler(PyArrayObject *obj) @@ -588,20 +589,7 @@ PyDataMem_GetHandler(PyArrayObject *obj) return handler; } #endif - /* Try to find a handler */ - PyArrayObject *base = obj; - handler = PyArray_HANDLER(obj); - while (handler == NULL) { - base = (PyArrayObject*)PyArray_BASE(base); - /* - * If the base is an array which owns its own data, return its allocator. - */ - if (base == NULL || ! PyArray_Check(base)) { - break; - } - handler = PyArray_HANDLER(base); - } - return handler; + return PyArray_HANDLER(obj); } NPY_NO_EXPORT PyObject * @@ -617,7 +605,10 @@ get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args) } const PyDataMem_Handler * mem_handler = PyDataMem_GetHandler((PyArrayObject *)arr); if (mem_handler == NULL) { - return NULL; + if (PyErr_Occurred()) { + return NULL; + } + Py_RETURN_NONE; } return PyUnicode_FromString(mem_handler->name); } diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 4cc46bb13f8f..918a8c92433d 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -843,8 +843,7 @@ PyArray_NewFromDescr_int( /* The handlers should never be called in this case */ fa->mem_handler = NULL; /* - * If data is passed in, this object won't own it by default. - * Caller must arrange for this to be reset if truly desired + * If data is passed in, this object won't own it. */ fa->flags &= ~NPY_ARRAY_OWNDATA; } @@ -3412,6 +3411,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre dptr += dtype->elsize; if (num < 0 && thisbuf == size) { totalbytes += bytes; + /* The handler is always valid */ tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes, &PyArray_HANDLER(r)->allocator); if (tmp == NULL) { @@ -3435,6 +3435,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre const size_t nsize = PyArray_MAX(*nread,1)*dtype->elsize; if (nsize != 0) { + /* The handler is always valid */ tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize, &PyArray_HANDLER(r)->allocator); if (tmp == NULL) { @@ -3541,6 +3542,7 @@ PyArray_FromFile(FILE *fp, PyArray_Descr *dtype, npy_intp num, char *sep) const size_t nsize = PyArray_MAX(nread,1) * dtype->elsize; char *tmp; + /* The handler is always valid */ if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize, &PyArray_HANDLER(ret)->allocator)) == NULL) { Py_DECREF(dtype); @@ -3826,6 +3828,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) */ elcount = (i >> 1) + (i < 4 ? 4 : 2) + i; if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) { + /* The handler is always valid */ new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes, &PyArray_HANDLER(ret)->allocator); } @@ -3868,6 +3871,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) /* The size cannot be zero for realloc. */ goto done; } + /* The handler is always valid */ new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize, &PyArray_HANDLER(ret)->allocator); if (new_data == NULL) { diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index 3d28df10b79a..96a1bc82df77 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -393,8 +393,14 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) PyArray_Descr *dtype = PyArray_DESCR(self); nbytes = dtype->elsize ? dtype->elsize : 1; } - PyDataMem_UserFREE(PyArray_DATA(self), nbytes, - &PyArray_HANDLER(self)->allocator); + PyDataMem_Handler *handler = PyArray_HANDLER(self); + if (handler == NULL) { + /* This can happen if someone arbitrarily sets NPY_ARRAY_OWNDATA */ + PyErr_SetString(PyExc_RuntimeError, + "no memory handler found but OWNDATA flag set"); + return -1; + } + PyDataMem_UserFREE(PyArray_DATA(self), nbytes, &handler->allocator); } if (PyArray_BASE(self)) { if ((PyArray_FLAGS(self) & NPY_ARRAY_WRITEBACKIFCOPY) || diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index aee03729b4a2..36979725db7a 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2053,8 +2053,14 @@ array_setstate(PyArrayObject *self, PyObject *args) * Allocation will never be 0, see comment in ctors.c * line 820 */ - PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, - &PyArray_HANDLER(self)->allocator); + PyDataMem_Handler *handler = PyArray_HANDLER(self); + if (handler == NULL) { + /* This can happen if someone arbitrarily sets NPY_ARRAY_OWNDATA */ + PyErr_SetString(PyExc_RuntimeError, + "no memory handler found but OWNDATA flag set"); + return NULL; + } + PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, &handler->allocator); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } Py_XDECREF(PyArray_BASE(self)); diff --git a/numpy/core/src/multiarray/shape.c b/numpy/core/src/multiarray/shape.c index 09cec841aed2..d1d2a8f2ca96 100644 --- a/numpy/core/src/multiarray/shape.c +++ b/numpy/core/src/multiarray/shape.c @@ -120,9 +120,16 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, } /* Reallocate space if needed - allocating 0 is forbidden */ + PyDataMem_Handler *handler = PyArray_HANDLER(self); + if (handler == NULL) { + /* This can happen if someone arbitrarily sets NPY_ARRAY_OWNDATA */ + PyErr_SetString(PyExc_RuntimeError, + "no memory handler found but OWNDATA flag set"); + return NULL; + } new_data = PyDataMem_UserRENEW(PyArray_DATA(self), newnbytes == 0 ? elsize : newnbytes, - &PyArray_HANDLER(self)->allocator); + &handler->allocator); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory for array"); diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 2361598e3d23..7df6adb5ed1f 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -151,12 +151,14 @@ def test_set_policy(get_module): orig_policy_name = np.core.multiarray.get_handler_name() a = np.arange(10).reshape((2, 5)) # a doesn't own its own data - assert np.core.multiarray.get_handler_name(a) == orig_policy_name + assert np.core.multiarray.get_handler_name(a) == None + assert np.core.multiarray.get_handler_name(a.base) == orig_policy_name orig_policy = get_module.set_secret_data_policy() b = np.arange(10).reshape((2, 5)) # b doesn't own its own data - assert np.core.multiarray.get_handler_name(b) == 'secret_data_allocator' + assert np.core.multiarray.get_handler_name(b) == None + assert np.core.multiarray.get_handler_name(b.base) == 'secret_data_allocator' if orig_policy_name == 'default_allocator': get_module.set_old_policy(None) # tests PyDataMem_SetHandler(NULL) @@ -171,16 +173,17 @@ def test_policy_propagation(get_module): class MyArr(np.ndarray): pass + # The memory policy goes hand-in-hand with flags.owndata orig_policy_name = np.core.multiarray.get_handler_name() - a = np.arange(10).view(MyArr).reshape((2, 5)) # a doesn't own its own data - assert np.core.multiarray.get_handler_name(a) == orig_policy_name + a = np.arange(10).view(MyArr).reshape((2, 5)) + assert np.core.multiarray.get_handler_name(a) == None + assert a.flags.owndata == False - orig_policy = get_module.set_secret_data_policy() - secret_policy_name = np.core.multiarray.get_handler_name() - b = np.arange(10).view(MyArr).reshape((2, 5)) # b doesn't own its own data - assert np.core.multiarray.get_handler_name(b) == secret_policy_name - get_module.set_old_policy(orig_policy) + assert np.core.multiarray.get_handler_name(a.base) == None + assert a.base.flags.owndata == False + assert np.core.multiarray.get_handler_name(a.base.base) == orig_policy_name + assert a.base.base.flags.owndata == True async def concurrent_context1(get_module, orig_policy_name, event): if orig_policy_name == 'default_allocator': diff --git a/numpy/core/tests/test_nditer.py b/numpy/core/tests/test_nditer.py index 73deb2357d6c..3d241e4dbd02 100644 --- a/numpy/core/tests/test_nditer.py +++ b/numpy/core/tests/test_nditer.py @@ -3149,6 +3149,7 @@ def test_partial_iteration_cleanup(in_dtype, buf_dtype, steps): # Note that resetting does not free references del it break_cycles() + break_cycles() assert count == sys.getrefcount(value) # Repeat the test with `iternext` @@ -3159,6 +3160,7 @@ def test_partial_iteration_cleanup(in_dtype, buf_dtype, steps): del it # should ensure cleanup break_cycles() + break_cycles() assert count == sys.getrefcount(value) From 3a97d9a423eb564360bd2a881e918b86dd422267 Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 26 Aug 2021 07:43:02 +0300 Subject: [PATCH 71/76] improve docstring, fix flake8 for tests --- numpy/core/_add_newdocs.py | 4 ++-- numpy/core/tests/test_mem_policy.py | 34 ++++++++++++++++------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py index 6da695d23ac2..b28632f2b63e 100644 --- a/numpy/core/_add_newdocs.py +++ b/numpy/core/_add_newdocs.py @@ -4693,8 +4693,8 @@ get_handler_name(a: ndarray) -> str,None Return the name of the memory handler used by `a`. If not provided, return - the name of the current global memory handler that will be used to allocate - data for the next `ndarray`. May return None if `a` does not own its + the name of the memory handler that will be used to allocate data for the + next `ndarray` in this context. May return None if `a` does not own its memory, in which case you can traverse ``a.base`` for a memory handler. """) diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 7df6adb5ed1f..4eaacd50c81c 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -148,42 +148,46 @@ def get_module(tmp_path): def test_set_policy(get_module): - orig_policy_name = np.core.multiarray.get_handler_name() + + get_handler_name = np.core.multiarray.get_handler_name + orig_policy_name = get_handler_name() a = np.arange(10).reshape((2, 5)) # a doesn't own its own data - assert np.core.multiarray.get_handler_name(a) == None - assert np.core.multiarray.get_handler_name(a.base) == orig_policy_name + assert get_handler_name(a) is None + assert get_handler_name(a.base) == orig_policy_name orig_policy = get_module.set_secret_data_policy() b = np.arange(10).reshape((2, 5)) # b doesn't own its own data - assert np.core.multiarray.get_handler_name(b) == None - assert np.core.multiarray.get_handler_name(b.base) == 'secret_data_allocator' + assert get_handler_name(b) is None + assert get_handler_name(b.base) == 'secret_data_allocator' if orig_policy_name == 'default_allocator': get_module.set_old_policy(None) # tests PyDataMem_SetHandler(NULL) - assert np.core.multiarray.get_handler_name() == 'default_allocator' + assert get_handler_name() == 'default_allocator' else: get_module.set_old_policy(orig_policy) - assert np.core.multiarray.get_handler_name() == orig_policy_name + assert get_handler_name() == orig_policy_name def test_policy_propagation(get_module): + # The memory policy goes hand-in-hand with flags.owndata class MyArr(np.ndarray): pass - # The memory policy goes hand-in-hand with flags.owndata - orig_policy_name = np.core.multiarray.get_handler_name() + get_handler_name = np.core.multiarray.get_handler_name + orig_policy_name = get_handler_name() a = np.arange(10).view(MyArr).reshape((2, 5)) - assert np.core.multiarray.get_handler_name(a) == None - assert a.flags.owndata == False + assert get_handler_name(a) is None + assert a.flags.owndata is False + + assert get_handler_name(a.base) is None + assert a.base.flags.owndata is False - assert np.core.multiarray.get_handler_name(a.base) == None - assert a.base.flags.owndata == False + assert get_handler_name(a.base.base) == orig_policy_name + assert a.base.base.flags.owndata is True - assert np.core.multiarray.get_handler_name(a.base.base) == orig_policy_name - assert a.base.base.flags.owndata == True async def concurrent_context1(get_module, orig_policy_name, event): if orig_policy_name == 'default_allocator': From 1df805c21ea193ad63fcc4d43abe3ec8e86dfd9f Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 26 Aug 2021 17:34:03 +0300 Subject: [PATCH 72/76] update PyDataMem_GetHandler() from review --- doc/neps/nep-0049.rst | 10 ++-- doc/source/reference/c-api/data_memory.rst | 7 +-- numpy/core/src/multiarray/alloc.c | 56 +++++++++++----------- numpy/core/src/multiarray/arraytypes.c.src | 2 +- numpy/core/src/multiarray/ctors.c | 2 +- numpy/core/src/multiarray/item_selection.c | 4 +- numpy/core/src/multiarray/methods.c | 4 +- 7 files changed, 39 insertions(+), 46 deletions(-) diff --git a/doc/neps/nep-0049.rst b/doc/neps/nep-0049.rst index 4a5edae81163..3c65bf12ddae 100644 --- a/doc/neps/nep-0049.rst +++ b/doc/neps/nep-0049.rst @@ -105,7 +105,7 @@ on the pointer to the data memory. The name of the handler will be exposed on the python level via a ``numpy.core.multiarray.get_handler_name(arr)`` function. If called as ``numpy.core.multiarray.get_handler_name()`` it will return the name of the -global handler that will be used to allocate data for the next new `ndarrray`. +handler that will be used to allocate data for the next new `ndarrray`. NumPy C-API functions ===================== @@ -153,12 +153,10 @@ NumPy C-API functions hooks. All the function pointers must be filled in, ``NULL`` is not accepted. -.. c:function:: const char * PyDataMem_GetHandlerName(PyArrayObject *obj) - - Return the const char name of the ``PyDataMem_Handler`` used by the - ``PyArrayObject``. If ``NULL``, return the name of the current global policy - that will be used to allocate data for the next ``PyArrayObject``. +.. c:function:: const PyDataMem_Handler * PyDataMem_GetHandler() + Return the current policy that will be used to allocate data for the + next ``PyArrayObject``. On failure, return ``NULL``. Sample code =========== diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index 7c6efa409017..43cfeae763ab 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -85,10 +85,9 @@ reallocate or free the data memory of the instance. so they will still call the python and numpy memory management callback hooks. -.. c:function:: const PyDataMem_Handler * PyDataMem_GetHandler(PyArrayObject *obj) +.. c:function:: const PyDataMem_Handler * PyDataMem_GetHandler() - Return the `PyDataMem_Handler` used by the ``PyArrayObject``. If ``NULL``, - return the current global policy that will be used to allocate data for the + Return the current policy that will be used to allocate data for the next ``PyArrayObject``. On failure, return ``NULL``. For an example of setting up and using the PyDataMem_Handler, see the test in @@ -98,8 +97,6 @@ For an example of setting up and using the PyDataMem_Handler, see the test in This function will be called during data memory manipulation - - .. c:function:: PyDataMem_EventHookFunc * PyDataMem_SetEventHook(PyDataMem_EventHookFunc *newhook, void *user_data, void **old_data) Sets the allocation event hook for numpy array data. diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 309bf020740b..fa7ce87c4095 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -552,44 +552,36 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) } /*NUMPY_API - * Return the PyDataMem_Handler used by obj. If obj is NULL, return - * the current global policy that will be used to allocate data - * for the next PyArrayObject. On failure, return NULL. Can also return NULL - * if obj does not own its memory + * Return the policy that will be used to allocate data + * for the next PyArrayObject. On failure, return NULL. */ NPY_NO_EXPORT const PyDataMem_Handler * -PyDataMem_GetHandler(PyArrayObject *obj) +PyDataMem_GetHandler() { PyObject *capsule; PyDataMem_Handler *handler; #if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) - if (obj == NULL) { - if (PyContextVar_Get(current_handler, NULL, &capsule)) { - return NULL; - } - handler = (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); - Py_DECREF(capsule); - return handler; + if (PyContextVar_Get(current_handler, NULL, &capsule)) { + return NULL; } + handler = (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); + Py_DECREF(capsule); + return handler; #else - PyObject *p; - if (obj == NULL) { - p = PyThreadState_GetDict(); - if (p == NULL) { - return NULL; - } - capsule = PyDict_GetItemString(p, "current_allocator"); - if (capsule == NULL) { - handler = &default_handler; - } - else { - handler = (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); - Py_DECREF(capsule); - } - return handler; + PyObject *p = PyThreadState_GetDict(); + if (p == NULL) { + return NULL; + } + capsule = PyDict_GetItemString(p, "current_allocator"); + if (capsule == NULL) { + handler = &default_handler; + } + else { + handler = (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); + Py_DECREF(capsule); } + return handler; #endif - return PyArray_HANDLER(obj); } NPY_NO_EXPORT PyObject * @@ -603,7 +595,13 @@ get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args) PyErr_SetString(PyExc_ValueError, "if supplied, argument must be an ndarray"); return NULL; } - const PyDataMem_Handler * mem_handler = PyDataMem_GetHandler((PyArrayObject *)arr); + const PyDataMem_Handler *mem_handler; + if (arr != NULL) { + mem_handler = PyArray_HANDLER(arr); + } + else{ + mem_handler = PyDataMem_GetHandler(); + } if (mem_handler == NULL) { if (PyErr_Occurred()) { return NULL; diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index a28974d6fbdf..75c1140e9a50 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3093,7 +3093,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (!PyArray_HASFIELDS(ap)) { return STRING_compare(ip1, ip2, ap); } - const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(NULL); + const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(); if (mem_handler == NULL) { goto finish; } diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 918a8c92433d..42633f5dd26c 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -807,7 +807,7 @@ PyArray_NewFromDescr_int( if (data == NULL) { /* Store the handler in case the default is modified */ - fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(NULL); + fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(); if (fa->mem_handler == NULL) { goto fail; } diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 395f3e074d6a..ee8de0bde5dd 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -963,7 +963,7 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, return 0; } - const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(NULL); + const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(); if (mem_handler == NULL) { return -1; } @@ -1096,7 +1096,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, NPY_BEGIN_THREADS_DEF; - const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(NULL); + const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(); if (mem_handler == NULL) { return NULL; } diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 36979725db7a..ed98c24fc24f 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2105,7 +2105,7 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_RETURN_NONE; } /* Store the handler in case the default is modified */ - fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(NULL); + fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(); if (fa->mem_handler == NULL) { Py_DECREF(rawdata); return NULL; @@ -2163,7 +2163,7 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_RETURN_NONE; } /* Store the functions in case the default handler is modified */ - fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(NULL); + fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(); if (fa->mem_handler == NULL) { return NULL; } From ef607bd0385af00bd8ffbd9db1d40cdb6469dcec Mon Sep 17 00:00:00 2001 From: Elias Koromilas Date: Fri, 27 Aug 2021 15:29:41 +0300 Subject: [PATCH 73/76] Implement allocator lifetime management --- doc/source/reference/c-api/data_memory.rst | 4 +- numpy/core/include/numpy/ndarraytypes.h | 8 +- numpy/core/src/multiarray/alloc.c | 140 ++++++++++--------- numpy/core/src/multiarray/alloc.h | 11 +- numpy/core/src/multiarray/arrayobject.c | 3 +- numpy/core/src/multiarray/arraytypes.c.src | 14 +- numpy/core/src/multiarray/ctors.c | 19 +-- numpy/core/src/multiarray/getset.c | 4 +- numpy/core/src/multiarray/item_selection.c | 21 +-- numpy/core/src/multiarray/methods.c | 18 ++- numpy/core/src/multiarray/multiarraymodule.c | 2 +- numpy/core/src/multiarray/shape.c | 4 +- numpy/core/tests/test_mem_policy.py | 23 ++- 13 files changed, 156 insertions(+), 115 deletions(-) diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst index 43cfeae763ab..8e29894035f3 100644 --- a/doc/source/reference/c-api/data_memory.rst +++ b/doc/source/reference/c-api/data_memory.rst @@ -77,7 +77,7 @@ reallocate or free the data memory of the instance. void (*free) (void *ctx, void *ptr, size_t size); } PyDataMemAllocator; -.. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) +.. c:function:: PyObject * PyDataMem_SetHandler(PyObject *handler) Set a new allocation policy. If the input value is ``NULL``, will reset the policy to the default. Return the previous policy, or @@ -85,7 +85,7 @@ reallocate or free the data memory of the instance. so they will still call the python and numpy memory management callback hooks. -.. c:function:: const PyDataMem_Handler * PyDataMem_GetHandler() +.. c:function:: PyObject * PyDataMem_GetHandler() Return the current policy that will be used to allocate data for the next ``PyArrayObject``. On failure, return ``NULL``. diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index a6e4fbe1a186..4705dd0d9de3 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -735,7 +735,7 @@ typedef struct tagPyArrayObject_fields { /* * For malloc/calloc/realloc/free per object */ - PyDataMem_Handler *mem_handler; + PyObject *mem_handler; } PyArrayObject_fields; /* @@ -1677,6 +1677,12 @@ PyArray_CLEARFLAGS(PyArrayObject *arr, int flags) ((PyArrayObject_fields *)arr)->flags &= ~flags; } +static NPY_INLINE NPY_RETURNS_BORROWED_REF PyObject * +PyArray_HANDLER(PyArrayObject *arr) +{ + return ((PyArrayObject_fields *)arr)->mem_handler; +} + #define PyTypeNum_ISBOOL(type) ((type) == NPY_BOOL) #define PyTypeNum_ISUNSIGNED(type) (((type) == NPY_UBYTE) || \ diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index fa7ce87c4095..17a9f44d1aa6 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -395,12 +395,16 @@ int uo_index=0; /* user_override index */ /* Wrappers for the default or any user-assigned PyDataMem_Handler */ NPY_NO_EXPORT void * -PyDataMem_UserNEW(size_t size, const PyDataMemAllocator *allocator) +PyDataMem_UserNEW(size_t size, PyObject *mem_handler) { void *result; + PyDataMem_Handler *handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + return NULL; + } assert(size != 0); - result = allocator->malloc(allocator->ctx, size); + result = handler->allocator.malloc(handler->allocator.ctx, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -415,10 +419,14 @@ PyDataMem_UserNEW(size_t size, const PyDataMemAllocator *allocator) } NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, const PyDataMemAllocator *allocator) +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyObject *mem_handler) { void *result; - result = allocator->calloc(allocator->ctx, nmemb, size); + PyDataMem_Handler *handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + return NULL; + } + result = handler->allocator.calloc(handler->allocator.ctx, nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -433,10 +441,14 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, const PyDataMemAllocator *al } NPY_NO_EXPORT void -PyDataMem_UserFREE(void *ptr, size_t size, const PyDataMemAllocator *allocator) +PyDataMem_UserFREE(void *ptr, size_t size, PyObject *mem_handler) { + PyDataMem_Handler *handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + return; + } PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); - allocator->free(allocator->ctx, ptr, size); + handler->allocator.free(handler->allocator.ctx, ptr, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API @@ -449,12 +461,16 @@ PyDataMem_UserFREE(void *ptr, size_t size, const PyDataMemAllocator *allocator) } NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, const PyDataMemAllocator *allocator) +PyDataMem_UserRENEW(void *ptr, size_t size, PyObject *mem_handler) { void *result; + PyDataMem_Handler *handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + return NULL; + } assert(size != 0); - result = allocator->realloc(allocator->ctx, ptr, size); + result = handler->allocator.realloc(handler->allocator.ctx, ptr, size); if (result != ptr) { PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); } @@ -478,37 +494,28 @@ PyDataMem_UserRENEW(void *ptr, size_t size, const PyDataMemAllocator *allocator) * functions so they will still call the python and numpy * memory management callback hooks. */ -NPY_NO_EXPORT const PyDataMem_Handler * -PyDataMem_SetHandler(PyDataMem_Handler *handler) +NPY_NO_EXPORT PyObject * +PyDataMem_SetHandler(PyObject *handler) { - PyObject *capsule; - PyObject *old_capsule; - PyDataMem_Handler *old_handler; + PyObject *old_handler; #if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) PyObject *token; - if (PyContextVar_Get(current_handler, NULL, &old_capsule)) { + if (PyContextVar_Get(current_handler, NULL, &old_handler)) { return NULL; } - old_handler = (PyDataMem_Handler *) PyCapsule_GetPointer(old_capsule, NULL); - Py_DECREF(old_capsule); - if (old_handler == NULL) { - return NULL; - } - if (handler != NULL) { - capsule = PyCapsule_New(handler, NULL, NULL); - if (capsule == NULL) { + if (handler == NULL) { + handler = PyCapsule_New(&default_handler, "mem_handler", NULL); + if (handler == NULL) { return NULL; } } else { - capsule = PyCapsule_New(&default_handler, NULL, NULL); - if (capsule == NULL) { - return NULL; - } + Py_INCREF(handler); } - token = PyContextVar_Set(current_handler, capsule); - Py_DECREF(capsule); + token = PyContextVar_Set(current_handler, handler); + Py_DECREF(handler); if (token == NULL) { + Py_DECREF(old_handler); return NULL; } Py_DECREF(token); @@ -519,32 +526,30 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) if (p == NULL) { return NULL; } - old_capsule = PyDict_GetItemString(p, "current_allocator"); - if (old_capsule == NULL) { - old_handler = &default_handler; - } - else { - old_handler = (PyDataMem_Handler *) PyCapsule_GetPointer(old_capsule, NULL); - Py_DECREF(old_capsule); + old_handler = PyDict_GetItemString(p, "current_allocator"); + if (old_handler == NULL) { + old_handler = PyCapsule_New(&default_handler, "mem_handler", NULL); if (old_handler == NULL) { return NULL; } } - if (handler != NULL) { - capsule = PyCapsule_New(handler, NULL, NULL); - if (capsule == NULL) { + else { + Py_INCREF(old_handler); + } + if (handler == NULL) { + handler = PyCapsule_New(&default_handler, "mem_handler", NULL); + if (handler == NULL) { + Py_DECREF(old_handler); return NULL; } } else { - capsule = PyCapsule_New(&default_handler, NULL, NULL); - if (capsule == NULL) { - return NULL; - } + Py_INCREF(handler); } - const int error = PyDict_SetItemString(p, "current_allocator", capsule); - Py_DECREF(capsule); + const int error = PyDict_SetItemString(p, "current_allocator", handler); + Py_DECREF(handler); if (error) { + Py_DECREF(old_handler); return NULL; } return old_handler; @@ -555,30 +560,29 @@ PyDataMem_SetHandler(PyDataMem_Handler *handler) * Return the policy that will be used to allocate data * for the next PyArrayObject. On failure, return NULL. */ -NPY_NO_EXPORT const PyDataMem_Handler * +NPY_NO_EXPORT PyObject * PyDataMem_GetHandler() { - PyObject *capsule; - PyDataMem_Handler *handler; + PyObject *handler; #if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) - if (PyContextVar_Get(current_handler, NULL, &capsule)) { + if (PyContextVar_Get(current_handler, NULL, &handler)) { return NULL; } - handler = (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); - Py_DECREF(capsule); return handler; #else PyObject *p = PyThreadState_GetDict(); if (p == NULL) { return NULL; } - capsule = PyDict_GetItemString(p, "current_allocator"); - if (capsule == NULL) { - handler = &default_handler; + handler = PyDict_GetItemString(p, "current_allocator"); + if (handler == NULL) { + handler = PyCapsule_New(&default_handler, "mem_handler", NULL); + if (handler == NULL) { + return NULL; + } } else { - handler = (PyDataMem_Handler *) PyCapsule_GetPointer(capsule, NULL); - Py_DECREF(capsule); + Py_INCREF(handler); } return handler; #endif @@ -595,18 +599,28 @@ get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args) PyErr_SetString(PyExc_ValueError, "if supplied, argument must be an ndarray"); return NULL; } - const PyDataMem_Handler *mem_handler; + PyObject *mem_handler; + PyDataMem_Handler *handler; + PyObject *name; if (arr != NULL) { - mem_handler = PyArray_HANDLER(arr); + mem_handler = PyArray_HANDLER((PyArrayObject *) arr); + if (mem_handler == NULL) { + Py_RETURN_NONE; + } + Py_INCREF(mem_handler); } - else{ + else { mem_handler = PyDataMem_GetHandler(); - } - if (mem_handler == NULL) { - if (PyErr_Occurred()) { + if (mem_handler == NULL) { return NULL; } - Py_RETURN_NONE; } - return PyUnicode_FromString(mem_handler->name); + handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + Py_DECREF(mem_handler); + return NULL; + } + name = PyUnicode_FromString(handler->name); + Py_DECREF(mem_handler); + return name; } diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index cbe042e7643d..96e4f6cbc852 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -10,16 +10,16 @@ NPY_NO_EXPORT PyObject * _set_madvise_hugepage(PyObject *NPY_UNUSED(self), PyObject *enabled_obj); NPY_NO_EXPORT void * -PyDataMem_UserNEW(npy_uintp sz, const PyDataMemAllocator *allocator); +PyDataMem_UserNEW(npy_uintp sz, PyObject *mem_handler); NPY_NO_EXPORT void * -PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, const PyDataMemAllocator *allocator); +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyObject *mem_handler); NPY_NO_EXPORT void -PyDataMem_UserFREE(void * p, npy_uintp sd, const PyDataMemAllocator *allocator); +PyDataMem_UserFREE(void * p, npy_uintp sd, PyObject *mem_handler); NPY_NO_EXPORT void * -PyDataMem_UserRENEW(void *ptr, size_t size, const PyDataMemAllocator *allocator); +PyDataMem_UserRENEW(void *ptr, size_t size, PyObject *mem_handler); NPY_NO_EXPORT void * npy_alloc_cache_dim(npy_uintp sz); @@ -47,7 +47,4 @@ extern PyDataMem_Handler default_handler; NPY_NO_EXPORT PyObject * get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj); - -#define PyArray_HANDLER(arr) ((PyArrayObject_fields*)(arr))->mem_handler - #endif diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index 37fd54611a6a..279cc285b84f 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -501,7 +501,8 @@ array_dealloc(PyArrayObject *self) if (nbytes == 0) { nbytes = fa->descr->elsize ? fa->descr->elsize : 1; } - PyDataMem_UserFREE(fa->data, nbytes, &fa->mem_handler->allocator); + PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler); + Py_DECREF(fa->mem_handler); } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 75c1140e9a50..42c8802d03d2 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3093,7 +3093,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (!PyArray_HASFIELDS(ap)) { return STRING_compare(ip1, ip2, ap); } - const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(); + PyObject *mem_handler = PyDataMem_GetHandler(); if (mem_handler == NULL) { goto finish; } @@ -3123,7 +3123,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) * create temporary buffer and copy, * always use the current handler for internal allocations */ - nip1 = PyDataMem_UserNEW(new->elsize, &mem_handler->allocator); + nip1 = PyDataMem_UserNEW(new->elsize, mem_handler); if (nip1 == NULL) { goto finish; } @@ -3136,12 +3136,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) * create temporary buffer and copy, * always use the current handler for internal allocations */ - nip2 = PyDataMem_UserNEW(new->elsize, &mem_handler->allocator); + nip2 = PyDataMem_UserNEW(new->elsize, mem_handler); if (nip2 == NULL) { if (nip1 != ip1 + offset) { /* destroy temporary buffer */ - PyDataMem_UserFREE(nip1, new->elsize, - &mem_handler->allocator); + PyDataMem_UserFREE(nip1, new->elsize, mem_handler); } goto finish; } @@ -3154,11 +3153,11 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { /* destroy temporary buffer */ - PyDataMem_UserFREE(nip1, new->elsize, &mem_handler->allocator); + PyDataMem_UserFREE(nip1, new->elsize, mem_handler); } if (nip2 != ip2 + offset) { /* destroy temporary buffer */ - PyDataMem_UserFREE(nip2, new->elsize, &mem_handler->allocator); + PyDataMem_UserFREE(nip2, new->elsize, mem_handler); } } if (res != 0) { @@ -3167,6 +3166,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } finish: + Py_XDECREF(mem_handler); return res; } diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 42633f5dd26c..c5b4963aa9e5 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -725,6 +725,7 @@ PyArray_NewFromDescr_int( fa->nd = nd; fa->dimensions = NULL; fa->data = NULL; + fa->mem_handler = NULL; if (data == NULL) { fa->flags = NPY_ARRAY_DEFAULT; @@ -807,7 +808,7 @@ PyArray_NewFromDescr_int( if (data == NULL) { /* Store the handler in case the default is modified */ - fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(); + fa->mem_handler = PyDataMem_GetHandler(); if (fa->mem_handler == NULL) { goto fail; } @@ -826,11 +827,10 @@ PyArray_NewFromDescr_int( * which could also be sub-fields of a VOID array */ if (zeroed || PyDataType_FLAGCHK(descr, NPY_NEEDS_INIT)) { - data = PyDataMem_UserNEW_ZEROED(nbytes, 1, - &fa->mem_handler->allocator); + data = PyDataMem_UserNEW_ZEROED(nbytes, 1, fa->mem_handler); } else { - data = PyDataMem_UserNEW(nbytes, &fa->mem_handler->allocator); + data = PyDataMem_UserNEW(nbytes, fa->mem_handler); } if (data == NULL) { raise_memory_error(fa->nd, fa->dimensions, descr); @@ -911,6 +911,7 @@ PyArray_NewFromDescr_int( return (PyObject *)fa; fail: + Py_XDECREF(fa->mem_handler); Py_DECREF(fa); return NULL; } @@ -3413,7 +3414,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre totalbytes += bytes; /* The handler is always valid */ tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes, - &PyArray_HANDLER(r)->allocator); + PyArray_HANDLER(r)); if (tmp == NULL) { err = 1; break; @@ -3437,7 +3438,7 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre if (nsize != 0) { /* The handler is always valid */ tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize, - &PyArray_HANDLER(r)->allocator); + PyArray_HANDLER(r)); if (tmp == NULL) { err = 1; } @@ -3544,7 +3545,7 @@ PyArray_FromFile(FILE *fp, PyArray_Descr *dtype, npy_intp num, char *sep) /* The handler is always valid */ if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize, - &PyArray_HANDLER(ret)->allocator)) == NULL) { + PyArray_HANDLER(ret))) == NULL) { Py_DECREF(dtype); Py_DECREF(ret); return PyErr_NoMemory(); @@ -3830,7 +3831,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) { /* The handler is always valid */ new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes, - &PyArray_HANDLER(ret)->allocator); + PyArray_HANDLER(ret)); } else { new_data = NULL; @@ -3873,7 +3874,7 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) } /* The handler is always valid */ new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize, - &PyArray_HANDLER(ret)->allocator); + PyArray_HANDLER(ret)); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate array memory"); diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index 96a1bc82df77..a8b0d80d6174 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -393,14 +393,14 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) PyArray_Descr *dtype = PyArray_DESCR(self); nbytes = dtype->elsize ? dtype->elsize : 1; } - PyDataMem_Handler *handler = PyArray_HANDLER(self); + PyObject *handler = PyArray_HANDLER(self); if (handler == NULL) { /* This can happen if someone arbitrarily sets NPY_ARRAY_OWNDATA */ PyErr_SetString(PyExc_RuntimeError, "no memory handler found but OWNDATA flag set"); return -1; } - PyDataMem_UserFREE(PyArray_DATA(self), nbytes, &handler->allocator); + PyDataMem_UserFREE(PyArray_DATA(self), nbytes, handler); } if (PyArray_BASE(self)) { if ((PyArray_FLAGS(self) & NPY_ARRAY_WRITEBACKIFCOPY) || diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index ee8de0bde5dd..0ec606000a22 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -963,18 +963,19 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, return 0; } - const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(); + PyObject *mem_handler = PyDataMem_GetHandler(); if (mem_handler == NULL) { return -1; } it = (PyArrayIterObject *)PyArray_IterAllButAxis((PyObject *)op, &axis); if (it == NULL) { + Py_DECREF(mem_handler); return -1; } size = it->size; if (needcopy) { - buffer = PyDataMem_UserNEW(N * elsize, &mem_handler->allocator); + buffer = PyDataMem_UserNEW(N * elsize, mem_handler); if (buffer == NULL) { ret = -1; goto fail; @@ -1059,12 +1060,13 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); /* cleanup internal buffer */ - PyDataMem_UserFREE(buffer, N * elsize, &mem_handler->allocator); + PyDataMem_UserFREE(buffer, N * elsize, mem_handler); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); } Py_DECREF(it); + Py_DECREF(mem_handler); return ret; } @@ -1096,7 +1098,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, NPY_BEGIN_THREADS_DEF; - const PyDataMem_Handler *mem_handler = PyDataMem_GetHandler(); + PyObject *mem_handler = PyDataMem_GetHandler(); if (mem_handler == NULL) { return NULL; } @@ -1105,6 +1107,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, PyArray_NDIM(op), PyArray_DIMS(op), NULL, NULL, 0, (PyObject *)op); if (rop == NULL) { + Py_DECREF(mem_handler); return NULL; } rstride = PyArray_STRIDE(rop, axis); @@ -1112,6 +1115,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, /* Check if there is any argsorting to do */ if (N <= 1 || PyArray_SIZE(op) == 0) { + Py_DECREF(mem_handler); memset(PyArray_DATA(rop), 0, PyArray_NBYTES(rop)); return (PyObject *)rop; } @@ -1125,7 +1129,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = PyDataMem_UserNEW(N * elsize, &mem_handler->allocator); + valbuffer = PyDataMem_UserNEW(N * elsize, mem_handler); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1134,7 +1138,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, if (needidxbuffer) { idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), - &mem_handler->allocator); + mem_handler); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1224,8 +1228,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); /* cleanup internal buffers */ - PyDataMem_UserFREE(valbuffer, N * elsize, &mem_handler->allocator); - PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), &mem_handler->allocator); + PyDataMem_UserFREE(valbuffer, N * elsize, mem_handler); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), mem_handler); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ @@ -1236,6 +1240,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, } Py_XDECREF(it); Py_XDECREF(rit); + Py_DECREF(mem_handler); return (PyObject *)rop; } diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index ed98c24fc24f..2b10a77c010d 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -2053,14 +2053,14 @@ array_setstate(PyArrayObject *self, PyObject *args) * Allocation will never be 0, see comment in ctors.c * line 820 */ - PyDataMem_Handler *handler = PyArray_HANDLER(self); + PyObject *handler = PyArray_HANDLER(self); if (handler == NULL) { /* This can happen if someone arbitrarily sets NPY_ARRAY_OWNDATA */ PyErr_SetString(PyExc_RuntimeError, "no memory handler found but OWNDATA flag set"); return NULL; } - PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, &handler->allocator); + PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, handler); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } Py_XDECREF(PyArray_BASE(self)); @@ -2105,13 +2105,15 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_RETURN_NONE; } /* Store the handler in case the default is modified */ - fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(); + Py_XDECREF(fa->mem_handler); + fa->mem_handler = PyDataMem_GetHandler(); if (fa->mem_handler == NULL) { Py_DECREF(rawdata); return NULL; } - fa->data = PyDataMem_UserNEW(num, &PyArray_HANDLER(fa)->allocator); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(self)); if (PyArray_DATA(self) == NULL) { + Py_DECREF(fa->mem_handler); Py_DECREF(rawdata); return PyErr_NoMemory(); } @@ -2148,6 +2150,7 @@ array_setstate(PyArrayObject *self, PyObject *args) } else { /* The handlers should never be called in this case */ + Py_XDECREF(fa->mem_handler); fa->mem_handler = NULL; fa->data = datastr; if (PyArray_SetBaseObject(self, rawdata) < 0) { @@ -2163,12 +2166,14 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_RETURN_NONE; } /* Store the functions in case the default handler is modified */ - fa->mem_handler = (PyDataMem_Handler *) PyDataMem_GetHandler(); + Py_XDECREF(fa->mem_handler); + fa->mem_handler = PyDataMem_GetHandler(); if (fa->mem_handler == NULL) { return NULL; } - fa->data = PyDataMem_UserNEW(num, &PyArray_HANDLER(fa)->allocator); + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(self)); if (PyArray_DATA(self) == NULL) { + Py_DECREF(fa->mem_handler); return PyErr_NoMemory(); } if (PyDataType_FLAGCHK(PyArray_DESCR(self), NPY_NEEDS_INIT)) { @@ -2177,6 +2182,7 @@ array_setstate(PyArrayObject *self, PyObject *args) PyArray_ENABLEFLAGS(self, NPY_ARRAY_OWNDATA); fa->base = NULL; if (_setlist_pkl(self, rawdata) < 0) { + Py_DECREF(fa->mem_handler); return NULL; } } diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index 23bf178d6fd3..6e4ebce41101 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -4912,7 +4912,7 @@ PyMODINIT_FUNC PyInit__multiarray_umath(void) { /* * Initialize the context-local PyDataMem_Handler capsule. */ - c_api = PyCapsule_New(&default_handler, NULL, NULL); + c_api = PyCapsule_New(&default_handler, "mem_handler", NULL); if (c_api == NULL) { goto err; } diff --git a/numpy/core/src/multiarray/shape.c b/numpy/core/src/multiarray/shape.c index d1d2a8f2ca96..4f4d74136bd6 100644 --- a/numpy/core/src/multiarray/shape.c +++ b/numpy/core/src/multiarray/shape.c @@ -120,7 +120,7 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, } /* Reallocate space if needed - allocating 0 is forbidden */ - PyDataMem_Handler *handler = PyArray_HANDLER(self); + PyObject *handler = PyArray_HANDLER(self); if (handler == NULL) { /* This can happen if someone arbitrarily sets NPY_ARRAY_OWNDATA */ PyErr_SetString(PyExc_RuntimeError, @@ -129,7 +129,7 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, } new_data = PyDataMem_UserRENEW(PyArray_DATA(self), newnbytes == 0 ? elsize : newnbytes, - &handler->allocator); + handler); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory for array"); diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index 4eaacd50c81c..a6f988fa79ac 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -17,16 +17,27 @@ def get_module(tmp_path): pytest.skip('link fails on cygwin') functions = [ ("set_secret_data_policy", "METH_NOARGS", """ - const PyDataMem_Handler *old = - PyDataMem_SetHandler(&secret_data_handler); - return PyCapsule_New((void *) old, NULL, NULL); + PyObject *secret_data = + PyCapsule_New(&secret_data_handler, "mem_handler", NULL); + if (secret_data == NULL) { + return NULL; + } + PyObject *old = PyDataMem_SetHandler(secret_data); + Py_DECREF(secret_data); + return old; """), ("set_old_policy", "METH_O", """ - PyDataMem_Handler *old = NULL; + PyObject *old; if (args != NULL && PyCapsule_CheckExact(args)) { - old = (PyDataMem_Handler *) PyCapsule_GetPointer(args, NULL); + old = PyDataMem_SetHandler(args); + } + else { + old = PyDataMem_SetHandler(NULL); + } + if (old == NULL) { + return NULL; } - PyDataMem_SetHandler(old); + Py_DECREF(old); Py_RETURN_NONE; """), ] From a3256e589af7895ce85f1d86fbb03c7be3b3c353 Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 29 Aug 2021 15:40:27 +0300 Subject: [PATCH 74/76] update NEP and add best-effort handling of error in PyDataMem_UserFREE --- doc/neps/nep-0049.rst | 20 +++++++++++--------- numpy/core/src/multiarray/alloc.c | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/doc/neps/nep-0049.rst b/doc/neps/nep-0049.rst index 3c65bf12ddae..6d05ad15eb18 100644 --- a/doc/neps/nep-0049.rst +++ b/doc/neps/nep-0049.rst @@ -93,14 +93,16 @@ High level design Users who wish to change the NumPy data memory management routines will use :c:func:`PyDataMem_SetHandler`, which uses a :c:type:`PyDataMem_Handler` -structure to hold pointers to functions used to manage the data memory. +structure to hold pointers to functions used to manage the data memory. In +order to allow lifetime management of the ``context``, the structure is wrapped +in a ``PyCapsule``. Since a call to ``PyDataMem_SetHandler`` will change the default functions, but that function may be called during the lifetime of an ``ndarray`` object, each -``ndarray`` will carry with it the ``PyDataMem_Handler`` struct used at the -time of its instantiation, and these will be used to reallocate or free the -data memory of the instance. Internally NumPy may use ``memcpy`` or ``memset`` -on the pointer to the data memory. +``ndarray`` will carry with it the ``PyDataMem_Handler``-wrapped PyCapsule used +at the time of its instantiation, and these will be used to reallocate or free +the data memory of the instance. Internally NumPy may use ``memcpy`` or +``memset`` on the pointer to the data memory. The name of the handler will be exposed on the python level via a ``numpy.core.multiarray.get_handler_name(arr)`` function. If called as @@ -144,16 +146,16 @@ NumPy C-API functions The consumer of the `PyDataMemAllocator` interface must keep track of ``size`` and make sure it is consistent with the parameter passed to the ``(m|c|re)alloc`` functions. -.. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) +.. c:function:: PyObject * PyDataMem_SetHandler(PyObject *handler) Sets a new allocation policy. If the input value is ``NULL``, will reset - the policy to the default. Returns the previous policy, ``NULL`` if the - previous policy was the default. We wrap the user-provided functions + the policy to the default. Return the previous policy, or + return NULL if an error has occurred. We wrap the user-provided so they will still call the Python and NumPy memory management callback hooks. All the function pointers must be filled in, ``NULL`` is not accepted. -.. c:function:: const PyDataMem_Handler * PyDataMem_GetHandler() +.. c:function:: const PyObject * PyDataMem_GetHandler() Return the current policy that will be used to allocate data for the next ``PyArrayObject``. On failure, return ``NULL``. diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index 17a9f44d1aa6..ea6de4f09dd8 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -440,11 +440,33 @@ PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyObject *mem_handler) return result; } +/* Similar to array_dealloc in arrayobject.c */ +static NPY_INLINE void +WARN_IN_FREE(PyObject* warning, const char * msg) { + if (PyErr_WarnEx(warning, msg, 1) < 0) { + PyObject * s; + + s = PyUnicode_FromString("PyDataMem_UserFREE"); + if (s) { + PyErr_WriteUnraisable(s); + Py_DECREF(s); + } + else { + PyErr_WriteUnraisable(Py_None); + } + } +} + + + NPY_NO_EXPORT void PyDataMem_UserFREE(void *ptr, size_t size, PyObject *mem_handler) { PyDataMem_Handler *handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); if (handler == NULL) { + WARN_IN_FREE(PyExc_RuntimeWarning, + "Could not get pointer to 'mem_handler' from PyCapsule"); + PyErr_Clear(); return; } PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); From 442b0e1b44f707003e92e87ad465b9b97bc7e3fb Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 25 Oct 2021 18:53:45 +0300 Subject: [PATCH 75/76] ENH: fix and test for blindly taking ownership of data --- numpy/core/src/multiarray/arrayobject.c | 13 +++++++-- numpy/core/tests/test_mem_policy.py | 35 ++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index f9ef0ddabd22..c3615f95fa99 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -501,8 +501,17 @@ array_dealloc(PyArrayObject *self) if (nbytes == 0) { nbytes = fa->descr->elsize ? fa->descr->elsize : 1; } - PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler); - Py_DECREF(fa->mem_handler); + if (fa->mem_handler == NULL) { + char const * msg = "Trying to dealloc data, but a memory policy " + "is not set. If you take ownership of the data, you must " + "also set a memory policy."; + WARN_IN_DEALLOC(PyExc_RuntimeWarning, msg); + // Guess at malloc/free ??? + free(fa->data); + } else { + PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler); + Py_DECREF(fa->mem_handler); + } } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py index a6f988fa79ac..a2ff98ceb781 100644 --- a/numpy/core/tests/test_mem_policy.py +++ b/numpy/core/tests/test_mem_policy.py @@ -1,8 +1,10 @@ import asyncio +import gc import pytest import numpy as np import threading -from numpy.testing import extbuild +import warnings +from numpy.testing import extbuild, assert_warns import sys @@ -40,6 +42,25 @@ def get_module(tmp_path): Py_DECREF(old); Py_RETURN_NONE; """), + ("get_array", "METH_NOARGS", """ + char *buf = (char *)malloc(20); + npy_intp dims[1]; + dims[0] = 20; + PyArray_Descr *descr = PyArray_DescrNewFromType(NPY_UINT8); + return PyArray_NewFromDescr(&PyArray_Type, descr, 1, dims, NULL, + buf, NPY_ARRAY_WRITEABLE, NULL); + """), + ("set_own", "METH_O", """ + if (!PyArray_Check(args)) { + PyErr_SetString(PyExc_ValueError, + "need an ndarray"); + return NULL; + } + PyArray_ENABLEFLAGS((PyArrayObject*)args, NPY_ARRAY_OWNDATA); + // Maybe try this too? + // PyArray_BASE(PyArrayObject *)args) = NULL; + Py_RETURN_NONE; + """), ] prologue = ''' #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION @@ -305,3 +326,15 @@ def test_new_policy(get_module): c = np.arange(10) assert np.core.multiarray.get_handler_name(c) == orig_policy_name + +def test_switch_owner(get_module): + a = get_module.get_array() + assert np.core.multiarray.get_handler_name(a) is None + get_module.set_own(a) + with warnings.catch_warnings(): + warnings.filterwarnings('always') + # The policy should be NULL, so we have to assume we can call "free" + with assert_warns(RuntimeWarning) as w: + del a + gc.collect() + print(w) From 8ca8b54bdb45f6cb09565a09ea2393237a4cc76b Mon Sep 17 00:00:00 2001 From: Sebastian Berg Date: Mon, 25 Oct 2021 13:51:24 -0500 Subject: [PATCH 76/76] Update doc/neps/nep-0049.rst --- doc/neps/nep-0049.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/neps/nep-0049.rst b/doc/neps/nep-0049.rst index 5b87c6d5a3b9..277351e3b30a 100644 --- a/doc/neps/nep-0049.rst +++ b/doc/neps/nep-0049.rst @@ -180,7 +180,6 @@ In practice, this means the handler must be immortal. As an implementation detail, currently this ``ContextVar`` contains a ``PyCapsule`` object storing a pointer to a ``PyDataMem_Handler`` with no destructor, but this should not be relied upon. ->>>>>>> main Sample code ===========