Cython 类型的内存视图:它们到底是什么?

Cython typed memoryviews: what they really are?

Cython documentation 很好地解释了它们的用途、如何声明它们以及如何使用它们。

然而,我仍然不清楚它们到底是什么。例如,像这样的 numpy 数组的简单赋值:

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

可以使 my_arr 的 accessing/assignment 更快。

幕后发生了什么? Numpy 应该已经以连续的方式分配内存中的元素,那么内存视图有什么用呢?显然没那么多,实际上numpy数组new_arr的memoryview赋值应该等价于

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

在速度方面。然而,内存视图被认为比 numpy 数组缓冲区更通用;你能举一个简单的例子,其中添加的 'generalization' 是 important/interesting 吗?

此外,如果我已经分配了一个指针以使事情尽可能快,那么将其转换为类型化内存视图有什么好处? (这个问题的答案可能和上面那个一样)

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

什么是内存视图:

当你在函数中写入时:

cdef double[:] a

你最终得到 __Pyx_memviewslice object:

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 object (struct __pyx_memoryview_obj *memview;) 的指针。如果数据由 Python object 拥有,则 memview 持有对该数据的引用并确保持有数据的 Python object 保持活动状态因为内存视图就在附近。

指向原始数据的指针的组合,以及如何对其进行索引的信息(shapestridessuboffsets)允许 Cython 使用原始数据进行索引数据指针和一些简单的 C 数学(非常有效)。例如:

x=a[0]

给出类似的东西:

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

相比之下,如果您使用无类型的 objects 并编写如下内容:

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

本身扩展为一大堆 Python C-api 调用(所以很慢)。最终它调用 a__getitem__ 方法。


与类型化的 numpy 数组相比:确实没有太大区别。 如果你这样做:

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

它的工作原理实际上非常像内存视图,可以访问原始指针并且速度应该非常相似。

使用 memoryviews 的好处是您可以使用更广泛的数组类型(例如 standard library array),因此您可以更灵活地调用函数的类型。这符合 "duck-typing" 的一般 Python 想法——您的代码应该使用任何行为正确的参数(而不是检查类型)。

第二个(小)优势是您不需要 numpy headers 来构建您的模块。

第三个(可能更大)优点是内存视图可以在没有 GIL 的情况下初始化,而 cdef np.ndarrays 不能 (http://docs.cython.org/src/userguide/memoryviews.html#comparison-to-the-old-buffer-support)

内存视图的一个小缺点是它们的设置似乎稍微慢一些。


与仅使用 malloced 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's 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. 您可以做的另一件事是处理定义为指针指针的二维数组(例如 double**)。参见 http://docs.cython.org/src/userguide/memoryviews.html#specifying-more-general-memory-layouts。我通常不喜欢这种类型的数组,但是如果你有现有的 C 代码已经使用了 if 那么你可以与之交互(并将它传回 Python 这样你的 Python 代码也可以使用它)。