Simplified creation of NumPy arrays from pre-allocated memory.

About 18 months ago, I wrote a blog-post that illustrates how to create a NumPy array that refers to another block of pre-allocated memory by creating a small low-level Python object that wraps around the memory and correctly deallocates the memory once NumPy is done with it.

The basic idea was to create a new Python object in C and point the base member of the NumPy array structure to this newly created object after using PyArray_SimpleNewFromData to create the array from a block of pre-allocated memory. NumPy will decref the object pointed to by its base member when it is destroyed. The example I provided created a new Python object in C and pointed the base member to it. That way, when the NumPy array is destroyed, the tp_dealloc function will be called on the newly created object.

The solution I provided is very flexible and also illustrates how to create a new Python object in C. However, as Lisandro Dalcin pointed out in the comments to that post, there is a simpler way to do it if you just need to call a particular deallocation function to free the underlying memory (or decrease a reference) after the NumPy array is destroyed.

The simple solution is to use a low-level Python object that holds a pointer to anything as well as a destructor. This low-level object is called a CObject in Python 2.x. The equivalent in Python 3.x (backported to Python 2.7) is the Capsule object.

Creating a CObject in C code is very simple. You just call PyCObject_FromVoidPtr with an arbitrary pointer as the first argument and a destructor function as the second argument. The signature of this destructor function should take the arbitrary pointer as its only argument and return nothing. The Python Object returned can be assigned to the base member of the ndarray directly.

Using the CObject removes the need to create your own “dummy” Python Object just to handle calling some needed code after NumPy is done with the memory. Thus, the code example can be updated to just:

[sourcecode language=”c++”]
int nd=2;
npy_intp dims[2]={10,20};
size_t size;
PyObject arr=NULL;
void *mymem;

size = PyArray_MultiplyList(dims, nd);
mymem = _aligned_malloc(size, 16);
arr = PyArray_SimpleNewFromData(nd, dims, NPY_DOUBLE, mymem);
if (arr == NULL) {
_aligned_free(mymem);
Py_XDECREF(arr);
}
PyArray_BASE(arr) = PyCObject_FromVoidPtr(mymem, _aligned_free);
[/sourcecode]

For completeness, the original code containing the _aligned_malloc and
_aligned_free function calls is reproduced here:

[sourcecode language=”c++”]
#include
#define uintptr_t size_t

#define _NOT_POWER_OF_TWO(n) (((n) & ((n) – 1)))
#define _UI(p) ((uintptr_t) (p))
#define _CP(p) ((char *) p)

#define _PTR_ALIGN(p0, alignment) \
((void *) (((_UI(p0) + (alignment + sizeof(void*))) \
& (~_UI(alignment – 1)))))

/* pointer must sometimes be aligned; assume sizeof(void*) is a power of two */
#define _ORIG_PTR(p) (*(((void **) (_UI(p) & (~_UI(sizeof(void*) – 1)))) – 1))

static void *_aligned_malloc(size_t size, size_t alignment) {
void *p0, *p;

if (_NOT_POWER_OF_TWO(alignment)) {
errno = EINVAL;
return ((void *) 0);
}

if (size == 0) {
return ((void *) 0);
}
if (alignment < sizeof(void *)) { alignment = sizeof(void *); } /* including the extra sizeof(void*) is overkill on a 32-bit machine, since malloc is already 8-byte aligned, as long as we enforce alignment >= 8 …but oh well */
p0 = malloc(size + (alignment + sizeof(void *)));
if (!p0) {
return ((void *) 0);
}
p = _PTR_ALIGN(p0, alignment);
_ORIG_PTR(p) = p0;
return p;
}

static void _aligned_free(void *memblock) {
if (memblock) {
free(_ORIG_PTR(memblock));
}
}
[/sourcecode]

Thanks to Lisandro for pointing out this simplified approach.

3 thoughts on “Simplified creation of NumPy arrays from pre-allocated memory.

  1. avatarMartin

    Thank you for this post, was looking for an easy and well working solution since a few days, and it seems that these is it, and it is so simple. I like it.

    Reply
  2. avatarJonathan

    PyArray_BASE(arr) = PyCObject_FromVoidPtr(mymem, _aligned_free);

    That doesn’t work… PyArray_BASE is a function call (can’t be an lvalue)…

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 101,042 bad guys.