Cython набрал memoryviews: что они на самом деле?

Cython документация объясняет очень хорошо, что они позволяют, как вы можете объявить их, и как использовать их.

Однако мне все еще не ясно, каковы они на самом деле. Например, простое назначение из массива numpy, например:

my_arr = np.empty(10, np.int32)
cdef int [:] new_arr = my_arr

может ускорить доступ/назначение my_arr.

Что это происходит за кулисами? Numpy должен уже распределять элементы в памяти смежным образом, так что же происходит с памятью? По-видимому, это не так, ведь присвоение памяти массиву numpy new_arr должно быть эквивалентно

cdef np.ndarray[np.int32_t, ndim=1] new_arr = np.empty(10, np.int32)

с точки зрения скорости. Тем не менее, memoryviews считаются более общими, чем numpy array buffer; вы могли бы сделать простой пример, в котором добавленное "обобщение" важно/интересно?

Кроме того, если я уже выделил указатель, чтобы сделать все как можно быстрее, в чем же преимущество приведения его к типизированной памяти? (ответ на этот вопрос может совпадать с ответом на этот вопрос)

cdef int *my_arr = <int *> malloc(N * sizeof(int))
cdef int[:] new_arr = <int[:N]>my_arr

Ответ 1

Что такое память:

Когда вы пишете в функции:

cdef double[:] a

вы __Pyx_memviewslice объект __Pyx_memviewslice:

typedef struct {
  struct __pyx_memoryview_obj *memview;
  char *data;
  Py_ssize_t shape[8];
  Py_ssize_t strides[8];
  Py_ssize_t suboffsets[8];
} __Pyx_memviewslice;

Memoryview содержит C-указатель некоторых данных, которые он (обычно) не имеет непосредственно. Он также содержит указатель на базовый объект Python (struct __pyx_memoryview_obj *memview;). Если данные принадлежат объекту Python, то memview ссылается на эту ссылку и гарантирует, что объект Python, который хранит данные, сохраняется в живых, пока вокруг памяти.

Комбинация указателя на необработанные данные и информация о том, как его индексировать (shape, strides и suboffsets), позволяет Cython делать индексирование с использованием указателей необработанных данных и некоторых простых математик C (что очень эффективно). например:

x=a[0]

дает что-то вроде:

(*((double *) ( /* dim=0 */ (__pyx_v_a.data + __pyx_t_2 * __pyx_v_a.strides[0]) )));

Напротив, если вы работаете с нетипизированными объектами и пишете что-то вроде:

a = np.array([1,2,3]) # note no typedef
x = x[0]

индексирование выполняется следующим образом:

__Pyx_GetItemInt(__pyx_v_a, 0, long, 1, __Pyx_PyInt_From_long, 0, 0, 1);

который сам расширяется до целых кучей вызовов C-api Python (так медленно). В конечном итоге это вызывает a __getitem__ метод.


По сравнению с типизированными массивами numpy: на самом деле нет большой разницы. Если вы сделаете что-то вроде:

cdef np.ndarray[np.int32_t, ndim=1] new_arr

он работает практически так же, как memoryview, с доступом к исходным указателям, и скорость должна быть очень схожей.

Преимущество использования памяти - то, что вы можете использовать с ними более широкий диапазон типов массивов (например, стандартный библиотечный массив), поэтому вы более гибко относитесь к типам, с которыми вы можете звонить. Это соответствует общей идее Python о "утиной печати" - что ваш код должен работать с любым параметром, который ведет себя правильно (вместо проверки типа).

Второе (небольшое) преимущество заключается в том, что вам не нужны заголовки numpy для создания вашего модуля.

Третьим (возможно большим) преимуществом является то, что память может быть инициализирована без GIL, а cdef np.ndarray не может (http://docs.cython.org/src/userguide/memoryviews.html#comparison-to-the-old -буфер-поддержка)

Небольшой недостаток памяти - это то, что они кажутся немного более медленными для настройки.


По сравнению с использованием malloc ed int указателей:

Вы не получите каких-либо преимуществ по скорости (но вы тоже не получите слишком большую скорость). Небольшими преимуществами преобразования с использованием памяти являются:

  1. Вы можете писать функции, которые могут использоваться либо с Python, либо внутри Cython:

    cpdef do_something_useful(double[:] x):
        # can be called from Python with any array type or from Cython
        # with something that already a memoryview
        ....
    
  2. Вы можете позволить Cython обрабатывать освобождение памяти для этого типа массива, что может упростить вашу жизнь для вещей, которые имеют неизвестную жизнь. См http://docs.cython.org/src/userguide/memoryviews.html#cython-arrays и особенно .callback_free_data.

  3. Вы можете передать свои данные обратно на python-код python (он получит базовый __pyx_memoryview_obj или что-то подобное). Будьте очень осторожны в управлении памятью здесь (см. Пункт 2!).

  4. Другая вещь, которую вы можете сделать, - это обработать такие вещи, как 2D-массивы, определенные как указатель на указатель (например, double**). См. Http://docs.cython.org/src/userguide/memoryviews.html#specifying-more-general-memory-layouts. Обычно мне не нравится этот тип массива, но если у вас уже есть C-код, который уже используется, то вы можете с ним взаимодействовать (и передать его обратно на Python, чтобы ваш код Python также мог его использовать).