Search code examples
cythongiltyped-memory-views

Reference counting of memoryviews with nogil


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?


Solution

  • 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.