Подсчет ссылок в памяти с помощью nogil

Я не совсем понимаю, как выполняется подсчет ссылок с памятью в больших/длинных разделах ног. Давайте предположим, что в основном весь мой код - это nogil, за исключением создания глубокого представления numpy-array-to-memoryview. Представление памяти возвращается и используется вверх.

Довольно простым примером может быть

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?

Я предполагаю, что когда someFun() возвращает представление памяти, счетчик ссылок массива numpy все еще должен быть равен 1. Как Cython впоследствии обрабатывает подсчет ссылок? Я имею в виду, что нельзя изменить счетчик ссылок, даже если представление памяти/массив разыменовано, верно? И как он узнает, что нужно разыменовать представление памяти, если выше было несколько слоев с кодом nogil, и, возможно, в отличие от someFun(), представление памяти не возвращается вверх?

EDIT: Итак, я нашел довольно грубый способ провести еще несколько тестов. Мой код теперь выглядит так.

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()

Интересная часть для меня заключается в том, что на 1-й остановке массив/просмотр памяти mv все еще выделяется, но когда я разыменовываю его, он освобождается до 2-й остановки. Я только проверил использование памяти с htop (поэтому массив выбран таким большим), возможно, есть лучший способ. Очевидно, что я хочу, чтобы это поведение free/refcounting произошло, но странно, что оно делает это, когда у него нет GIL. Может memoryviews не совсем ногил?

Может кто-нибудь объяснить, если это надежное поведение?


person oli    schedule 28.07.2020    source источник


Ответы (1)


Обновление счетчика ссылок представления памяти в блоке nogil происходит так же, как ваша функция someFun является nogil: она получает gil для обновления счетчика ссылок.

Линия

with nogil:
     mv = someFun(leng)

транслируется в следующий C-код:

__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;

для привязки к новому значению необходимо обновить подсчет ссылок для старого значения, что происходит в __PYX_XDEC_MEMVIEW. Его реализацию можно посмотреть здесь:

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);
        }
    ...
}

это означает, что если у нас нет gil (__Pyx_XDEC_MEMVIEW вызывается со вторым аргументом = 0), он будет получен, чтобы обеспечить правильный подсчет ссылок.

Следствием вышеизложенного является то, что повторное связывание представления памяти обходится недешево, поскольку для этого необходимо получить GIL, и поэтому его следует избегать в тесных циклах nogil.

person ead    schedule 28.07.2020
comment
Вероятно, стоит добавить, что во многих случаях не нужно повторно получать GIL, поскольку представление памяти сохраняет свой собственный внутренний атомарный счетчик получения. На самом деле это происходит только в особом случае, когда количество захватов падает до 0. Для уничтожения фрагмента, который указывает на живое представление памяти, не требуется GIL. - person DavidW; 28.07.2020
comment
@DavidW, ты прав. Поскольку я не совсем понимаю, что происходит с этим old_acquisition_count, я решил не углубляться в это. Но это довольно интересно, поэтому я обновлю свой ответ позже. - person ead; 28.07.2020