Creating an array or typed memoryview without holding the GIL?

880 views
Skip to first unread message

Robert McGibbon

unread,
Dec 9, 2014, 2:40:53 AM12/9/14
to cython...@googlegroups.com
Is it possible to allocate a typed memoryview without holding the GIL? I was hoping, for example, to use code like what is shown below, but the cast seems to require the gil.

```
from libc.stdlib cimport malloc, free

cdef double* p
cdef double[::1] X
cdef size_t N

with nogil:
        p = <double* >malloc(N*sizeof(double))
        X = <double[:N]> p
        [...]
        free(p)
```

Sturla Molden

unread,
Dec 9, 2014, 8:29:37 AM12/9/14
to cython...@googlegroups.com
On 09/12/14 08:40, Robert McGibbon wrote:
> Is it possible to allocate a typed memoryview without holding the GIL?

I don't think so. You can slice and index a typed memoryview without
holding the GIL, though. As I understand it (correct me if I am wrong),
casting a pointer to a typed memoryview will wrap the pointer with an
instance of cython.view.array, which is a Python object (and thus its
refcounting will require the GIL).


> from libc.stdlib cimport malloc, free

Be aware that libc.stdlib functions also require the GIL.


> cdef double* p
> cdef double[::1] X
> cdef size_t N
>
> with nogil:
> p = <double* >malloc(N*sizeof(double))
> X = <double[:N]> p
> [...]
> free(p)



Note that you can temporarily grab the GIL back. malloc and free also
needs protection against Python exceptions when coding in Cython (unlike
C but similar to C++). Thus I would end with something alike this:


```
# file heapmem.pyx

cdef extern from "Python.h":
ctypedef long Py_intptr_t
void *PyMem_Malloc(size_t)
void PyMem_Free(void*)


cdef class Heapmem:

"""
A class that wraps PyMem_Malloc and PyMem_Free for
safe use with Cython.
"""

cdef:
void *_pointer
Py_ssize_t size

def __cinit__(Heapmem self, Py_ssize_t n):
self._pointer = NULL
self._size = n

def __init__(Heapmem self, Py_ssize_t n):
self.allocate()

def allocate(Heapmem self):
if self._pointer != NULL:
raise RuntimeError("Memory already allocated")
else:
self._pointer = PyMem_Malloc(self._size)
if (self._pointer == NULL):
raise MemoryError()

def __dealloc__(Heapmem self):
if self._pointer != NULL:
PyMem_Free(self._pointer)
self._pointer = NULL

property pointer:
def __get__(Heapmem self):
if self._pointer != NULL:
return <Py_intptr_t> self._pointer
else:
raise RuntimeError("Memory not allocated")

property doublearray:
def __get__(Heapmem self):
cdef Py_ssize_t n = self._size//sizeof(double)
cdef double *dp = <double*> self._pointer
cdef double[::1] out
if self._pointer != NULL:
out = <double[:n:1]> dp
return out
else:
raise RuntimeError("Memory not allocated")

def __enter__(self):
if self._pointer != NULL:
raise RuntimeError("Memory not allocated")

def __exit__(Heapmem self, type, value, traceback):
if self._pointer != NULL:
PyMem_Free(self._pointer)
self._pointer = NULL
```

And then:


```
from heapmem import Heapmem
cdef double[::1] X
cdef Py_ssize_t N

# do stuff with the GIL
[...]

# explicitly release the GIL
with nogil:

# do stuff without the GIL
[...]

# explicitly grab the GIL back
with gil:

# do stuff with the GIL
[...]

# allocate memory
with Heapmem(N*sizeof(double)) as p:

# wrap pointer with cython.view.array and
# assign to typed memoryview
X = p.doublearray

# explicitly release the GIL
with nogil:

# do stuff without the GIL
[...]

# implicitly get the GIL back here

# do stuff with the GIL
[...]

# memory in is ALWAYS freed here, not postponed
# to when is garbage collected

# do stuff with the GIL
[...]

# implicitly release the GIL here

# do stuff without the GIL
[...]

# implicitly get the GIL back

# do stuff with the GIL
[...]

```

This allows us to finely control when we grab the GIL, and it is also
safe against memory leaks due to Python exceptions. If we just drop
malloc and free into Cython we have to be very careful to use
try-finally to ensure that a Python exception do not unpair them. A
context manager is much easier to work with.

Another thing is that wrapping malloc and free in a context manager
allows us to accurately control when memory is allocated and
deallocated. In particular, a reference leak will not produce a memory leak.


Sturla










Sturla Molden

unread,
Dec 9, 2014, 8:59:08 AM12/9/14
to cython...@googlegroups.com
On 09/12/14 14:29, Sturla Molden wrote:

> Another thing is that wrapping malloc and free in a context manager
> allows us to accurately control when memory is allocated and
> deallocated. In particular, a reference leak will not produce a memory
> leak.

And the advantage of using this approach over NumPy or cython.vew.array
is that we have full control over when the memory is deallocated. It is
not postponed to the garbage collection of the array object.

Sturla

Robert Bradshaw

unread,
Dec 9, 2014, 11:56:15 AM12/9/14
to cython...@googlegroups.com
Not sure circumventing garbage collection to manually control
reallocation would be considered by most to be a feature :). I suppose
sometimes it has advantages though.

- Robert

Sturla Molden

unread,
Dec 9, 2014, 1:05:02 PM12/9/14
to cython...@googlegroups.com
On 09/12/14 17:55, Robert Bradshaw wrote:

>> And the advantage of using this approach over NumPy or cython.vew.array is
>> that we have full control over when the memory is deallocated. It is not
>> postponed to the garbage collection of the array object.
>
> Not sure circumventing garbage collection to manually control
> reallocation would be considered by most to be a feature :). I suppose
> sometimes it has advantages though.

Perhaps not :)

It will make the memory volatile with respect to threads and view
arrays, so it can easily cause havoc. But if you have a large array you
might want to make sure that you get rid of its buffer at a particular
line of code.

I just suggested that NumPy supports this:

with np.zeros(n) as x:
[...]

I expect a downright rejection though. :-)

Below is a modified version of Heapmem that works better with NumPy,
i.e. it exposes the memory as an ndarray instead of cython.view.array.

with Heapmem(n * np.double().itemsize) as hm:
x = hm.doublearray
[...]


Sturla




from cpython cimport PyMem_Malloc, PyMem_Free
from libc.string cimport memset
cimport numpy as cnp
cnp.init_array()


cdef class Heapmem:

cdef:
void *_pointer
cnp.intp_t _size

def __cinit__(Heapmem self, Py_ssize_t n):
self._pointer = NULL
self._size = <cnp.intp_t> n

def __init__(Heapmem self, Py_ssize_t n):
self.allocate()

def allocate(Heapmem self):
if self._pointer != NULL:
raise RuntimeError("Memory already allocated")
else:
self._pointer = PyMem_Malloc(self._size)
if (self._pointer == NULL):
raise MemoryError()
memset(self._pointer, 0, self._size)

def __dealloc__(Heapmem self):
if self._pointer != NULL:
PyMem_Free(self._pointer)
self._pointer = NULL

property pointer:
def __get__(Heapmem self):
return <cnp.intp_t> self._pointer

property doublearray:
def __get__(Heapmem self):
cdef cnp.intp_t n = self._size//sizeof(double)
if self._pointer != NULL:
return cnp.PyArray_SimpleNewFromData(1, &n,
cnp.NPY_DOUBLE, self._pointer)
else:
raise RuntimeError("Memory not allocated")

property chararray:
def __get__(Heapmem self):
if self._pointer != NULL:
return cnp.PyArray_SimpleNewFromData(1, &self._size,
cnp.NPY_CHAR, self._pointer)
else:
raise RuntimeError("Memory not allocated")

# TODO: add similar properties for other dtypes
Reply all
Reply to author
Forward
0 new messages