I don't quite understand how reference counting is done with memoryviews in large/longer nogil sections. Let's assume basically all my code is nogil, except for the creation of a numpy-array-to-memoryview deep down. The memoryview is returned and used upwards.
A fairly simple example would be
import numpy as np
cdef:
double[::1] mv
cdef double[::1] someFun(int nn) nogil:
cdef:
double[::1] mvb
with gil:
mvb = np.arange(nn, dtype=np.double)
return mvb
with nogil:
mv = someFun(30)
# Here could be MUCH more "nogil" code
# How is memory management/reference counting done here?
I assume when someFun() returns the memoryview the refcount of the numpy array should still be at 1. How does Cython handle the refcounting afterwards? I mean it's not allowed to change the refcount even if the memoryview/array is dereferenced, right? And how would it know to dereference the memoryview if there were several layers with nogil code above, and maybe unlike to someFun() the memoryview isn't returned upwards?
EDIT: So I figured out a rather crude way to do some more testing. My code now looks like this.
import numpy as np
cdef extern from "stdio.h":
int getchar() nogil
int printf(const char* formatt, ...) nogil
cdef:
double[::1] mv, mv2 = np.ones(3)
int ii, leng = 140000000
cdef double[::1] someFun(int nn) nogil:
cdef:
double[::1] mvb
with gil:
mvb = np.ones(nn, dtype=np.double)
return mvb
with nogil:
mv = someFun(leng)
printf("1st stop")
getchar()
mv = mv2
printf("2nd stop")
getchar()
The interesting part for me is that at the 1st stop the array/memoryview mv
is still allocated, but when I dereference it gets free'd until 2nd stop. I only checked memory usage with htop
(that's why the array is chosen so large), there is probably a better way.
Obviously that free/refcounting behavior what I want to happen, but it's weird that it does it when it doesn't have the GIL. Maybe memoryviews are not completely nogil?
Can someone explain to if this is reliable behavior?
Updating reference count of the memoryview in the nogil-block happens the same way your function someFun
is nogil
: it acquires gil to update the reference count.
The line
with nogil:
mv = someFun(leng)
is translated to the following C-code:
__pyx_t_3 = __pyx_f_3foo_someFun(__pyx_v_3foo_leng); if (unlikely(!__pyx_t_3.memview)) __PYX_ERR(0, 18, __pyx_L3_error)
__PYX_XDEC_MEMVIEW(&__pyx_v_3foo_mv, 0);
__pyx_v_3foo_mv = __pyx_t_3;
__pyx_t_3.memview = NULL;
__pyx_t_3.data = NULL;
in order to bind to a new value, the reference counting for the old value must be updated, which happens in __PYX_XDEC_MEMVIEW
. Its implementation can be looked up here:
static CYTHON_INLINE void __Pyx_XDEC_MEMVIEW({{memviewslice_name}} *memslice,
int have_gil, int lineno) {
...
} else if (likely(old_acquisition_count == 1)) {
// Last slice => discard owned Python reference to memoryview object.
if (have_gil) {
Py_CLEAR(memslice->memview);
} else {
PyGILState_STATE _gilstate = PyGILState_Ensure();
Py_CLEAR(memslice->memview);
PyGILState_Release(_gilstate);
}
...
}
which means if we don't have gil (__Pyx_XDEC_MEMVIEW
called with second argument = 0
), it will be acquired to ensure that the reference counting is done properly.
The consequence of the above is, that rebinding a memory view is not cheap as it needs to acquire the GIL and thus should be avoided in tight nogil-loops.