summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatti Picus <matti.picus@gmail.com>2021-10-25 21:53:48 +0300
committerGitHub <noreply@github.com>2021-10-25 13:53:48 -0500
commit84e0707afa587e7655410561324ac36085db2b95 (patch)
tree9f7db2f514f0c33dcaece64076025de94b9e1c70
parent48e6ac6e120c6d408d85d4fdd3c4867e0195a758 (diff)
downloadnumpy-84e0707afa587e7655410561324ac36085db2b95.tar.gz
ENH: Configurable allocator (#17582)
Fixes gh-17467. Adds a public struct to hold memory manipulation routines PyDataMem_Handler and two new API functions PyDataMem_SetHandler to replace the current routines with the new ones, and PyDataMem_GetHandlerName to get the string name of the current routines (either globally or for a specific ndarray object). This also changes the size of the ndarray object to hold the PyDataMem_Handler active when it was created so subsequent actions on its data memory will remain consistent. Tests and documentation are included. Along the way, I found some places in the code where the current policy is inconsistent (all data memory handling should have gone through npy_*_cache not PyDataMem_*) so even if this is rejected it might improve the cache handling. The PyDataMem_Handler has fields to override memcpy, these are currently not implemented: memcpy in the code base is untouched. I think this PR is invasive enough as-is, if desired memcpy can be handled in a follow-up PR. * ENH: add and use global configurable memory routines * ENH: add tests and a way to compile c-extensions from tests * fix allocation/free exposed by tests * DOC: document the new APIs (and some old ones too) * BUG: return void from FREE, also some cleanup * MAINT: changes from review * fixes from linter * setting ndarray->descr on 0d or scalars mess with FREE * make scalar allocation more consistent wrt np_alloc_cache * change formatting for sphinx * remove memcpy variants * update to match NEP 49 * ENH: add a python-level get_handler_name * ENH: add core.multiarray.get_handler_name * Allow closure-like definition of the data mem routines * Fix incompatible pointer warnings * Note PyDataMemAllocator and PyMemAllocatorEx differentiation Co-authored-by: Matti Picus <matti.picus@gmail.com> * Redefine default allocator handling * Always allocate new arrays using the current_handler * Search for the mem_handler name of the data owner * Sub-comparisons don't need a local mem_handler * Make the default_handler a valid PyDataMem_Handler * Fix PyDataMem_SetHandler description (NEP discussion) * Pass the allocators by reference * Implement allocator context-locality * Fix documentation, make PyDataMem_GetHandler return const * remove import of setuptools==49.1.3, doesn't work on python3.10 * Fix refcount leaks * fix function signatures in test * Return early on PyDataMem_GetHandler error (VOID_compare) * Add context/thread-locality tests, allow testing custom policies * ENH: add and use global configurable memory routines * ENH: add tests and a way to compile c-extensions from tests * fix allocation/free exposed by tests * DOC: document the new APIs (and some old ones too) * BUG: return void from FREE, also some cleanup * MAINT: changes from review * fixes from linter * setting ndarray->descr on 0d or scalars mess with FREE * make scalar allocation more consistent wrt np_alloc_cache * change formatting for sphinx * remove memcpy variants * update to match NEP 49 * ENH: add a python-level get_handler_name * ENH: add core.multiarray.get_handler_name * Allow closure-like definition of the data mem routines * Fix incompatible pointer warnings * Note PyDataMemAllocator and PyMemAllocatorEx differentiation Co-authored-by: Matti Picus <matti.picus@gmail.com> * Redefine default allocator handling * Always allocate new arrays using the current_handler * Search for the mem_handler name of the data owner * Sub-comparisons don't need a local mem_handler * Make the default_handler a valid PyDataMem_Handler * Fix PyDataMem_SetHandler description (NEP discussion) * Pass the allocators by reference * remove import of setuptools==49.1.3, doesn't work on python3.10 * fix function signatures in test * try to fix cygwin extension building * YAPF mem_policy test * Less empty lines, more comments (tests) * Apply suggestions from code review (set an exception and) Co-authored-by: Matti Picus <matti.picus@gmail.com> * skip test on cygwin * update API hash for changed signature * TST: add gc.collect to make sure cycles are broken * Implement thread-locality for PyPy Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net> * Update numpy/core/tests/test_mem_policy.py Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net> * fixes from review * update circleci config * fix test * make the connection between OWNDATA and having a allocator handle more explicit * improve docstring, fix flake8 for tests * update PyDataMem_GetHandler() from review * Implement allocator lifetime management * update NEP and add best-effort handling of error in PyDataMem_UserFREE * ENH: fix and test for blindly taking ownership of data * Update doc/neps/nep-0049.rst Co-authored-by: Elias Koromilas <elias.koromilas@gmail.com>
-rw-r--r--doc/TESTS.rst.txt15
-rw-r--r--doc/neps/nep-0049.rst27
-rw-r--r--doc/source/reference/c-api/data_memory.rst119
-rw-r--r--doc/source/reference/c-api/index.rst1
-rw-r--r--numpy/core/_add_newdocs.py10
-rw-r--r--numpy/core/code_generators/cversions.txt4
-rw-r--r--numpy/core/code_generators/numpy_api.py9
-rw-r--r--numpy/core/include/numpy/ndarraytypes.h38
-rw-r--r--numpy/core/multiarray.py4
-rw-r--r--numpy/core/setup_common.py4
-rw-r--r--numpy/core/src/multiarray/alloc.c339
-rw-r--r--numpy/core/src/multiarray/alloc.h17
-rw-r--r--numpy/core/src/multiarray/arrayobject.c20
-rw-r--r--numpy/core/src/multiarray/arraytypes.c.src31
-rw-r--r--numpy/core/src/multiarray/convert_datatype.c2
-rw-r--r--numpy/core/src/multiarray/ctors.c41
-rw-r--r--numpy/core/src/multiarray/getset.c18
-rw-r--r--numpy/core/src/multiarray/item_selection.c33
-rw-r--r--numpy/core/src/multiarray/methods.c49
-rw-r--r--numpy/core/src/multiarray/multiarraymodule.c17
-rw-r--r--numpy/core/src/multiarray/scalartypes.c.src17
-rw-r--r--numpy/core/src/multiarray/shape.c12
-rw-r--r--numpy/core/tests/test_mem_policy.py340
-rw-r--r--numpy/core/tests/test_nditer.py6
-rw-r--r--numpy/ma/mrecords.py1
-rw-r--r--numpy/testing/__init__.py2
-rw-r--r--numpy/testing/_private/extbuild.py251
-rw-r--r--tools/lint_diff.ini3
28 files changed, 1351 insertions, 79 deletions
diff --git a/doc/TESTS.rst.txt b/doc/TESTS.rst.txt
index d048a4569..0d8137f4a 100644
--- a/doc/TESTS.rst.txt
+++ b/doc/TESTS.rst.txt
@@ -139,6 +139,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<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.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
+
+.. autofunction:: build_and_import_extension
+
Labeling tests
--------------
diff --git a/doc/neps/nep-0049.rst b/doc/neps/nep-0049.rst
index 51a3f11b1..277351e3b 100644
--- a/doc/neps/nep-0049.rst
+++ b/doc/neps/nep-0049.rst
@@ -93,19 +93,21 @@ 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
``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
=====================
@@ -150,20 +152,19 @@ NumPy C-API functions
15780_ and 15788_ but has not yet been resolved. When it is this NEP should
be revisited.
-.. 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(PyArrayObject *obj)
+.. c:function:: const PyObject * PyDataMem_GetHandler()
- Return the ``PyDataMem_Handler`` used by the
- ``PyArrayObject``. If ``NULL``, return the handler
- that will be used to allocate data for the next ``PyArrayObject``.
+ Return the current policy that will be used to allocate data for the
+ next ``PyArrayObject``. On failure, return ``NULL``.
``PyDataMem_Handler`` thread safety and lifetime
================================================
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 000000000..8e2989403
--- /dev/null
+++ b/doc/source/reference/c-api/data_memory.rst
@@ -0,0 +1,119 @@
+Memory management in NumPy
+==========================
+
+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
+after creating the python object in `__new__`. The ``strides`` and
+``shape`` are stored in a piece of memory allocated internally.
+
+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
+-------------------
+
+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.
+
+Since those early days, Python also improved its memory management
+capabilities, and began providing
+various :ref:`management policies <memoryoverview>` 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.
+
+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,
+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.
+
+.. 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 aligned */
+ PyDataMemAllocator allocator;
+ } PyDataMem_Handler;
+
+ where the allocator structure is
+
+ .. code-block:: c
+
+ /* The declaration of free differs from PyMemAllocatorEx */
+ typedef struct {
+ void *ctx;
+ void* (*malloc) (void *ctx, size_t size);
+ void* (*calloc) (void *ctx, size_t nelem, size_t elsize);
+ void* (*realloc) (void *ctx, void *ptr, size_t new_size);
+ void (*free) (void *ctx, void *ptr, size_t size);
+ } PyDataMemAllocator;
+
+.. 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
+ 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:: PyObject * PyDataMem_GetHandler()
+
+ 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
+: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 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.
+
+ 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 bb1ed154e..6288ff33b 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
diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py
index 50291c3e7..dbbb43009 100644
--- a/numpy/core/_add_newdocs.py
+++ b/numpy/core/_add_newdocs.py
@@ -4727,6 +4727,16 @@ add_newdoc('numpy.core.umath', '_add_newdoc_ufunc',
and then throwing away the ufunc.
""")
+add_newdoc('numpy.core.multiarray', 'get_handler_name',
+ """
+ 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 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.
+ """)
+
add_newdoc('numpy.core.multiarray', '_set_madvise_hugepage',
"""
_set_madvise_hugepage(enabled: bool) -> bool
diff --git a/numpy/core/code_generators/cversions.txt b/numpy/core/code_generators/cversions.txt
index a02c7153a..38ee4dac2 100644
--- a/numpy/core/code_generators/cversions.txt
+++ b/numpy/core/code_generators/cversions.txt
@@ -56,5 +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.22) Configurable memory allocations
+0x0000000f = 0c420aed67010594eb81f23ddfb02a88
diff --git a/numpy/core/code_generators/numpy_api.py b/numpy/core/code_generators/numpy_api.py
index fbd323368..3813c6ad7 100644
--- a/numpy/core/code_generators/numpy_api.py
+++ b/numpy/core/code_generators/numpy_api.py
@@ -76,9 +76,9 @@ multiarray_types_api = {
# 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,),
@@ -350,6 +350,9 @@ multiarray_funcs_api = {
'PyArray_ResolveWritebackIfCopy': (302,),
'PyArray_SetWritebackIfCopyBase': (303,),
# End 1.14 API
+ 'PyDataMem_SetHandler': (304,),
+ 'PyDataMem_GetHandler': (305,),
+ # End 1.21 API
}
ufunc_types_api = {
diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h
index 8d810fa64..80177e2bb 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
@@ -667,6 +665,24 @@ typedef struct _arr_descr {
} PyArray_ArrayDescr;
/*
+ * Memory handler structure for array data.
+ */
+/* The declaration of free differs from PyMemAllocatorEx */
+typedef struct {
+ void *ctx;
+ void* (*malloc) (void *ctx, size_t size);
+ void* (*calloc) (void *ctx, size_t nelem, size_t elsize);
+ void* (*realloc) (void *ctx, void *ptr, size_t new_size);
+ void (*free) (void *ctx, void *ptr, size_t size);
+} PyDataMemAllocator;
+
+typedef struct {
+ char name[128]; /* multiple of 64 to keep the struct aligned */
+ PyDataMemAllocator allocator;
+} PyDataMem_Handler;
+
+
+/*
* The main array object structure.
*
* It has been recommended to use the inline functions defined below
@@ -716,6 +732,10 @@ typedef struct tagPyArrayObject_fields {
/* For weak references */
PyObject *weakreflist;
void *_buffer_info; /* private buffer info, tagged to allow warning */
+ /*
+ * For malloc/calloc/realloc/free per object
+ */
+ PyObject *mem_handler;
} PyArrayObject_fields;
/*
@@ -1659,6 +1679,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/multiarray.py b/numpy/core/multiarray.py
index 154df6f4d..351cd3a1b 100644
--- a/numpy/core/multiarray.py
+++ b/numpy/core/multiarray.py
@@ -31,8 +31,8 @@ __all__ = [
'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/setup_common.py b/numpy/core/setup_common.py
index 85c8f16d1..70e8fc897 100644
--- a/numpy/core/setup_common.py
+++ b/numpy/core/setup_common.py
@@ -43,8 +43,8 @@ C_ABI_VERSION = 0x01000009
# 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 adb4ae128..e4756264d 100644
--- a/numpy/core/src/multiarray/alloc.c
+++ b/numpy/core/src/multiarray/alloc.c
@@ -133,9 +133,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);
@@ -145,7 +146,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;
}
@@ -187,8 +188,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.
@@ -254,21 +255,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;
}
@@ -316,3 +317,325 @@ 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 */
+ default_malloc, /* malloc */
+ default_calloc, /* calloc */
+ default_realloc, /* realloc */
+ default_free /* free */
+ }
+};
+
+#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600)
+PyObject *current_handler;
+#endif
+
+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, 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 = handler->allocator.malloc(handler->allocator.ctx, 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, PyObject *mem_handler)
+{
+ void *result;
+ 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
+ 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;
+}
+
+/* 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);
+ handler->allocator.free(handler->allocator.ctx, 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, 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 = handler->allocator.realloc(handler->allocator.ctx, 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
+ * Set a new allocation policy. If the input value is NULL, will reset
+ * 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.
+ */
+NPY_NO_EXPORT PyObject *
+PyDataMem_SetHandler(PyObject *handler)
+{
+ PyObject *old_handler;
+#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600)
+ PyObject *token;
+ if (PyContextVar_Get(current_handler, NULL, &old_handler)) {
+ return NULL;
+ }
+ if (handler == NULL) {
+ handler = PyCapsule_New(&default_handler, "mem_handler", NULL);
+ if (handler == NULL) {
+ return NULL;
+ }
+ }
+ else {
+ Py_INCREF(handler);
+ }
+ token = PyContextVar_Set(current_handler, handler);
+ Py_DECREF(handler);
+ if (token == NULL) {
+ Py_DECREF(old_handler);
+ return NULL;
+ }
+ Py_DECREF(token);
+ return old_handler;
+#else
+ PyObject *p;
+ p = PyThreadState_GetDict();
+ if (p == NULL) {
+ return NULL;
+ }
+ 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;
+ }
+ }
+ 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 {
+ Py_INCREF(handler);
+ }
+ const int error = PyDict_SetItemString(p, "current_allocator", handler);
+ Py_DECREF(handler);
+ if (error) {
+ Py_DECREF(old_handler);
+ return NULL;
+ }
+ return old_handler;
+#endif
+}
+
+/*NUMPY_API
+ * Return the policy that will be used to allocate data
+ * for the next PyArrayObject. On failure, return NULL.
+ */
+NPY_NO_EXPORT PyObject *
+PyDataMem_GetHandler()
+{
+ PyObject *handler;
+#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600)
+ if (PyContextVar_Get(current_handler, NULL, &handler)) {
+ return NULL;
+ }
+ return handler;
+#else
+ PyObject *p = PyThreadState_GetDict();
+ if (p == NULL) {
+ return NULL;
+ }
+ handler = PyDict_GetItemString(p, "current_allocator");
+ if (handler == NULL) {
+ handler = PyCapsule_New(&default_handler, "mem_handler", NULL);
+ if (handler == NULL) {
+ return NULL;
+ }
+ }
+ else {
+ Py_INCREF(handler);
+ }
+ return handler;
+#endif
+}
+
+NPY_NO_EXPORT PyObject *
+get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args)
+{
+ 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;
+ }
+ PyObject *mem_handler;
+ PyDataMem_Handler *handler;
+ PyObject *name;
+ if (arr != NULL) {
+ mem_handler = PyArray_HANDLER((PyArrayObject *) arr);
+ if (mem_handler == NULL) {
+ Py_RETURN_NONE;
+ }
+ Py_INCREF(mem_handler);
+ }
+ else {
+ mem_handler = PyDataMem_GetHandler();
+ if (mem_handler == NULL) {
+ return NULL;
+ }
+ }
+ 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 1259abca5..4f7df1f84 100644
--- a/numpy/core/src/multiarray/alloc.h
+++ b/numpy/core/src/multiarray/alloc.h
@@ -11,13 +11,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, PyObject *mem_handler);
NPY_NO_EXPORT void *
-npy_alloc_cache_zero(npy_uintp sz);
+PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyObject *mem_handler);
NPY_NO_EXPORT void
-npy_free_cache(void * p, npy_uintp sd);
+PyDataMem_UserFREE(void * p, npy_uintp sd, PyObject *mem_handler);
+
+NPY_NO_EXPORT void *
+PyDataMem_UserRENEW(void *ptr, size_t size, PyObject *mem_handler);
NPY_NO_EXPORT void *
npy_alloc_cache_dim(npy_uintp sz);
@@ -37,4 +40,12 @@ 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);
+
#endif /* NUMPY_CORE_SRC_MULTIARRAY_ALLOC_H_ */
diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c
index 28aff5d65..c3615f95f 100644
--- a/numpy/core/src/multiarray/arrayobject.c
+++ b/numpy/core/src/multiarray/arrayobject.c
@@ -493,7 +493,25 @@ array_dealloc(PyArrayObject *self)
if (PyDataType_FLAGCHK(fa->descr, NPY_ITEM_REFCOUNT)) {
PyArray_XDECREF(self);
}
- npy_free_cache(fa->data, PyArray_NBYTES(self));
+ /*
+ * 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;
+ }
+ 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/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src
index 15782a91b..9fe76845a 100644
--- a/numpy/core/src/multiarray/arraytypes.c.src
+++ b/numpy/core/src/multiarray/arraytypes.c.src
@@ -3093,6 +3093,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap)
if (!PyArray_HASFIELDS(ap)) {
return STRING_compare(ip1, ip2, ap);
}
+ PyObject *mem_handler = PyDataMem_GetHandler();
+ if (mem_handler == NULL) {
+ goto finish;
+ }
descr = PyArray_DESCR(ap);
/*
* Compare on the first-field. If equal, then
@@ -3107,15 +3111,19 @@ 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;
+
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);
+ /*
+ * create temporary buffer and copy,
+ * always use the current handler for internal allocations
+ */
+ nip1 = PyDataMem_UserNEW(new->elsize, mem_handler);
if (nip1 == NULL) {
goto finish;
}
@@ -3124,11 +3132,15 @@ 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 */
- nip2 = npy_alloc_cache(new->elsize);
+ /*
+ * create temporary buffer and copy,
+ * always use the current handler for internal allocations
+ */
+ nip2 = PyDataMem_UserNEW(new->elsize, mem_handler);
if (nip2 == NULL) {
if (nip1 != ip1 + offset) {
- npy_free_cache(nip1, new->elsize);
+ /* destroy temporary buffer */
+ PyDataMem_UserFREE(nip1, new->elsize, mem_handler);
}
goto finish;
}
@@ -3140,10 +3152,12 @@ 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);
+ /* destroy temporary buffer */
+ PyDataMem_UserFREE(nip1, new->elsize, mem_handler);
}
if (nip2 != ip2 + offset) {
- npy_free_cache(nip2, new->elsize);
+ /* destroy temporary buffer */
+ PyDataMem_UserFREE(nip2, new->elsize, mem_handler);
}
}
if (res != 0) {
@@ -3152,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/convert_datatype.c b/numpy/core/src/multiarray/convert_datatype.c
index eeadad374..3135d6989 100644
--- a/numpy/core/src/multiarray/convert_datatype.c
+++ b/numpy/core/src/multiarray/convert_datatype.c
@@ -2119,7 +2119,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 9da75fb8a..bb3569918 100644
--- a/numpy/core/src/multiarray/ctors.c
+++ b/numpy/core/src/multiarray/ctors.c
@@ -726,6 +726,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;
@@ -805,12 +806,19 @@ PyArray_NewFromDescr_int(
fa->flags |= NPY_ARRAY_C_CONTIGUOUS|NPY_ARRAY_F_CONTIGUOUS;
}
+
if (data == NULL) {
+ /* Store the handler in case the default is modified */
+ fa->mem_handler = PyDataMem_GetHandler();
+ if (fa->mem_handler == NULL) {
+ goto fail;
+ }
/*
* Allocate something even for zero-space arrays
* 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;
@@ -820,21 +828,23 @@ 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);
}
else {
- data = npy_alloc_cache(nbytes);
+ data = PyDataMem_UserNEW(nbytes, fa->mem_handler);
}
if (data == NULL) {
raise_memory_error(fa->nd, fa->dimensions, descr);
goto fail;
}
+
fa->flags |= NPY_ARRAY_OWNDATA;
}
else {
+ /* 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;
}
@@ -902,6 +912,7 @@ PyArray_NewFromDescr_int(
return (PyObject *)fa;
fail:
+ Py_XDECREF(fa->mem_handler);
Py_DECREF(fa);
return NULL;
}
@@ -3409,7 +3420,9 @@ 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);
+ /* The handler is always valid */
+ tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes,
+ PyArray_HANDLER(r));
if (tmp == NULL) {
err = 1;
break;
@@ -3431,7 +3444,9 @@ 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);
+ /* The handler is always valid */
+ tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize,
+ PyArray_HANDLER(r));
if (tmp == NULL) {
err = 1;
}
@@ -3536,7 +3551,9 @@ 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) {
+ /* The handler is always valid */
+ if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize,
+ PyArray_HANDLER(ret))) == NULL) {
Py_DECREF(dtype);
Py_DECREF(ret);
return PyErr_NoMemory();
@@ -3820,7 +3837,9 @@ 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);
+ /* The handler is always valid */
+ new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes,
+ PyArray_HANDLER(ret));
}
else {
new_data = NULL;
@@ -3858,10 +3877,12 @@ 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);
+ /* The handler is always valid */
+ new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize,
+ 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 2c8d1b3b4..e81ca2947 100644
--- a/numpy/core/src/multiarray/getset.c
+++ b/numpy/core/src/multiarray/getset.c
@@ -384,7 +384,23 @@ 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));
+ 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;
+ }
+ 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);
}
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 33d378c2b..086b674c8 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;
@@ -962,14 +963,19 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort,
return 0;
}
+ 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 = npy_alloc_cache(N * elsize);
+ buffer = PyDataMem_UserNEW(N * elsize, mem_handler);
if (buffer == NULL) {
ret = -1;
goto fail;
@@ -1053,12 +1059,14 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort,
fail:
NPY_END_THREADS_DESCR(PyArray_DESCR(op));
- npy_free_cache(buffer, N * elsize);
+ /* cleanup internal buffer */
+ 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;
}
@@ -1090,11 +1098,16 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort,
NPY_BEGIN_THREADS_DEF;
+ PyObject *mem_handler = PyDataMem_GetHandler();
+ 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,
0, (PyObject *)op);
if (rop == NULL) {
+ Py_DECREF(mem_handler);
return NULL;
}
rstride = PyArray_STRIDE(rop, axis);
@@ -1102,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;
}
@@ -1115,7 +1129,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, mem_handler);
if (valbuffer == NULL) {
ret = -1;
goto fail;
@@ -1123,7 +1137,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),
+ mem_handler);
if (idxbuffer == NULL) {
ret = -1;
goto fail;
@@ -1212,8 +1227,9 @@ _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));
+ /* cleanup internal buffers */
+ 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 */
@@ -1224,6 +1240,7 @@ fail:
}
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 391e65f6a..2d66c77dc 100644
--- a/numpy/core/src/multiarray/methods.c
+++ b/numpy/core/src/multiarray/methods.c
@@ -1975,6 +1975,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);
@@ -2041,7 +2051,18 @@ array_setstate(PyArrayObject *self, PyObject *args)
}
if ((PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA)) {
- PyDataMem_FREE(PyArray_DATA(self));
+ /*
+ * Allocation will never be 0, see comment in ctors.c
+ * line 820
+ */
+ 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);
PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA);
}
Py_XDECREF(PyArray_BASE(self));
@@ -2077,7 +2098,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)) {
@@ -2086,8 +2106,16 @@ array_setstate(PyArrayObject *self, PyObject *args)
Py_DECREF(rawdata);
Py_RETURN_NONE;
}
- fa->data = PyDataMem_NEW(num);
+ /* Store the handler in case the default is modified */
+ 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(self));
if (PyArray_DATA(self) == NULL) {
+ Py_DECREF(fa->mem_handler);
Py_DECREF(rawdata);
return PyErr_NoMemory();
}
@@ -2123,7 +2151,12 @@ array_setstate(PyArrayObject *self, PyObject *args)
Py_DECREF(rawdata);
}
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) {
+ Py_DECREF(rawdata);
return NULL;
}
}
@@ -2134,8 +2167,15 @@ array_setstate(PyArrayObject *self, PyObject *args)
if (num == 0 || elsize == 0) {
Py_RETURN_NONE;
}
- fa->data = PyDataMem_NEW(num);
+ /* Store the functions in case the default handler is modified */
+ Py_XDECREF(fa->mem_handler);
+ fa->mem_handler = PyDataMem_GetHandler();
+ if (fa->mem_handler == NULL) {
+ return NULL;
+ }
+ 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)) {
@@ -2144,6 +2184,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 d211f01bc..dea828ed9 100644
--- a/numpy/core/src/multiarray/multiarraymodule.c
+++ b/numpy/core/src/multiarray/multiarraymodule.c
@@ -4433,6 +4433,9 @@ static struct PyMethodDef array_module_methods[] = {
{"geterrobj",
(PyCFunction) ufunc_geterr,
METH_VARARGS, NULL},
+ {"get_handler_name",
+ (PyCFunction) get_handler_name,
+ METH_VARARGS, NULL},
{"_add_newdoc_ufunc", (PyCFunction)add_newdoc_ufunc,
METH_VARARGS, NULL},
{"_get_sfloat_dtype",
@@ -4910,6 +4913,20 @@ 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.
+ */
+ c_api = PyCapsule_New(&default_handler, "mem_handler", NULL);
+ if (c_api == NULL) {
+ goto err;
+ }
+ current_handler = PyContextVar_New("current_allocator", c_api);
+ Py_DECREF(c_api);
+ if (current_handler == NULL) {
+ goto err;
+ }
+#endif
return m;
err:
diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src
index 56f17431a..0d52211a8 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);
}
@@ -3151,7 +3161,10 @@ void_arrtype_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
(int) NPY_MAX_INT);
return NULL;
}
- destptr = npy_alloc_cache_zero(memu);
+ if (memu == 0) {
+ memu = 1;
+ }
+ 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 5a4e8c0f3..162abd6a4 100644
--- a/numpy/core/src/multiarray/shape.c
+++ b/numpy/core/src/multiarray/shape.c
@@ -121,8 +121,16 @@ 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);
+ 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;
+ }
+ new_data = PyDataMem_UserRENEW(PyArray_DATA(self),
+ newnbytes == 0 ? elsize : newnbytes,
+ 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
new file mode 100644
index 000000000..a2ff98ceb
--- /dev/null
+++ b/numpy/core/tests/test_mem_policy.py
@@ -0,0 +1,340 @@
+import asyncio
+import gc
+import pytest
+import numpy as np
+import threading
+import warnings
+from numpy.testing import extbuild, assert_warns
+import sys
+
+
+@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.
+ """
+ if sys.platform.startswith('cygwin'):
+ pytest.skip('link fails on cygwin')
+ functions = [
+ ("set_secret_data_policy", "METH_NOARGS", """
+ 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", """
+ PyObject *old;
+ if (args != NULL && PyCapsule_CheckExact(args)) {
+ old = PyDataMem_SetHandler(args);
+ }
+ else {
+ old = PyDataMem_SetHandler(NULL);
+ }
+ if (old == NULL) {
+ return NULL;
+ }
+ 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
+ #include <numpy/arrayobject.h>
+ /*
+ * 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 *);
+ } SecretDataAllocatorFuncs;
+
+ NPY_NO_EXPORT void *
+ shift_alloc(void *ctx, size_t sz) {
+ SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx;
+ char *real = (char *)funcs->malloc(sz + 64);
+ if (real == NULL) {
+ return NULL;
+ }
+ snprintf(real, 64, "originally allocated %ld", (unsigned long)sz);
+ return (void *)(real + 64);
+ }
+ NPY_NO_EXPORT void *
+ 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;
+ }
+ snprintf(real, 64, "originally allocated %ld via zero",
+ (unsigned long)sz);
+ return (void *)(real + 64);
+ }
+ NPY_NO_EXPORT void
+ shift_free(void *ctx, void * p, npy_uintp sz) {
+ SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx;
+ if (p == NULL) {
+ return ;
+ }
+ char *real = (char *)p - 64;
+ if (strncmp(real, "originally allocated", 20) != 0) {
+ fprintf(stdout, "uh-oh, unmatched shift_free, "
+ "no appropriate prefix\\n");
+ /* Make C runtime crash by calling free on the wrong address */
+ funcs->free((char *)p + 10);
+ /* funcs->free(real); */
+ }
+ else {
+ npy_uintp i = (npy_uintp)atoi(real +20);
+ if (i != sz) {
+ fprintf(stderr, "uh-oh, unmatched shift_free"
+ "(ptr, %ld) but allocated %ld\\n", sz, i);
+ /* This happens in some places, only print */
+ funcs->free(real);
+ }
+ else {
+ funcs->free(real);
+ }
+ }
+ }
+ NPY_NO_EXPORT void *
+ 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 *)funcs->realloc(real, sz + 64) + 64);
+ }
+ else {
+ char *real = (char *)funcs->realloc(p, sz + 64);
+ if (real == NULL) {
+ return NULL;
+ }
+ snprintf(real, 64, "originally allocated "
+ "%ld via realloc", (unsigned long)sz);
+ return (void *)(real + 64);
+ }
+ }
+ /* 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 secret_data_handler = {
+ "secret_data_allocator",
+ {
+ &secret_data_handler_ctx, /* ctx */
+ shift_alloc, /* malloc */
+ shift_zero, /* calloc */
+ shift_realloc, /* realloc */
+ shift_free /* free */
+ }
+ };
+ '''
+ more_init = "import_array();"
+ try:
+ import mem_policy
+ return mem_policy
+ 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)
+
+
+def test_set_policy(get_module):
+
+ 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 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 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 get_handler_name() == 'default_allocator'
+ else:
+ get_module.set_old_policy(orig_policy)
+ 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
+
+ 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 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 get_handler_name(a.base.base) == orig_policy_name
+ assert a.base.base.flags.owndata is True
+
+
+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
+ # 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, 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
+
+
+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))
+
+
+def concurrent_thread1(get_module, event):
+ 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,
+ args=(get_module, event))
+ concurrent_task1.start()
+ concurrent_task2.start()
+ 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
+
+
+@pytest.mark.slow
+def test_new_policy(get_module):
+ a = np.arange(10)
+ 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_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
+ #
+ # if needed, debug this by
+ # - running tests with -- -s (to not capture stdout/stderr
+ # - setting extra_argv=['-vv'] here
+ assert np.core.test('full', verbose=2, extra_argv=['-vv'])
+ # also try the ma tests, the pickling test is quite tricky
+ assert np.ma.test('full', verbose=2, extra_argv=['-vv'])
+
+ get_module.set_old_policy(orig_policy)
+
+ 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)
diff --git a/numpy/core/tests/test_nditer.py b/numpy/core/tests/test_nditer.py
index 79f44ef80..ed775cac6 100644
--- a/numpy/core/tests/test_nditer.py
+++ b/numpy/core/tests/test_nditer.py
@@ -9,7 +9,7 @@ import numpy.core._multiarray_tests as _multiarray_tests
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
)
@@ -3150,6 +3150,8 @@ 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 +3161,8 @@ def test_partial_iteration_cleanup(in_dtype, buf_dtype, steps):
it.iternext()
del it # should ensure cleanup
+ break_cycles()
+ break_cycles()
assert count == sys.getrefcount(value)
diff --git a/numpy/ma/mrecords.py b/numpy/ma/mrecords.py
index 2ce1f0a23..1e8103bcf 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
diff --git a/numpy/testing/__init__.py b/numpy/testing/__init__.py
index bca1d3670..a008f5828 100644
--- a/numpy/testing/__init__.py
+++ b/numpy/testing/__init__.py
@@ -10,7 +10,7 @@ from unittest import TestCase
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 000000000..8b3a438dd
--- /dev/null
+++ b/numpy/testing/_private/extbuild.py
@@ -0,0 +1,251 @@
+"""
+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 sysconfig
+from numpy.distutils.ccompiler import new_compiler
+from distutils.errors import CompileError
+
+__all__ = ['build_and_import_extension', '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 <Python.h>
+
+ %(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, verbose=2)
+ compiler.customize('')
+ 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
diff --git a/tools/lint_diff.ini b/tools/lint_diff.ini
index 3b66d3c3e..9e31050b7 100644
--- a/tools/lint_diff.ini
+++ b/tools/lint_diff.ini
@@ -1,4 +1,5 @@
[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