Cython类型化内存视图:它们究竟是什么?

27

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)

就速度而言,NumPy数组缓冲区比内存视图更快。然而,内存视图被认为比NumPy数组缓冲区更通用。你能否举个简单的例子来展示这种“通用性”很重要或有趣的情况呢?

此外,如果我已经分配了一个指针以尽可能提高速度,将其转换为类型化的内存视图有什么优势?(这个问题的答案可能与上面的问题相同)

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

1
文档的第一行写道:“类型化内存视图允许高效地访问内存缓冲区,例如那些底层的NumPy数组,而不会产生任何Python开销。”看起来memoryview只是cython自己的一组c函数,用于访问缓冲区,绕过了numpy函数。它不会比直接的c风格访问更快,但可能更容易使用。 - hpaulj
如果您查看编译后的CPP文件,就可以看到如何声明和使用memoryview。 - Den-Jason
1个回答

35

什么是memoryview:

当你在函数中写入以下代码:

cdef double[:] a

你最终得到一个__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;

The memoryview包含一个C指针和一些数据,它通常不直接拥有。它还包含一个指向底层Python对象(struct __pyx_memoryview_obj * memview;)的指针。如果数据由Python对象拥有,则memview保留对其的引用,并确保在memoryview存在时保持持有数据的Python对象存活。
指向原始数据的指针以及如何索引它的信息(shapestridessuboffsets的组合)允许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);

这本身会扩展到一堆Python C-api调用(因此很慢)。最终它会调用a__getitem__方法。


与numpy数组相比:实际上并没有太大的区别。 如果你做了这样的事情:

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

这个功能实际上很像memoryview,可以访问原始指针,速度应该非常相似。

使用memoryview的优点是可以使用更广泛的数组类型(例如标准库数组),因此您对可以调用函数的类型更加灵活。这符合Python的一般思想:“鸭子类型”-即您的代码应该与表现正确的任何参数一起工作(而不是检查类型)。

第二个(小)优点是构建模块时不需要numpy头文件。

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

内存视图的一个轻微劣势是它们似乎设置起来略慢。


与仅使用malloc的int指针相比:

您不会获得任何速度优势(但也不会有太多速度损失)。使用memoryview进行转换的微小优势包括:

  1. You can write functions that can be used either from Python or internally within 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. You can let Cython handle the freeing of memory for this type of array, which could simplify your life for things that have an unknown lifetime. See http://docs.cython.org/src/userguide/memoryviews.html#cython-arrays and especially .callback_free_data.

  3. You can pass your data back to python python code (it'll get the underlying __pyx_memoryview_obj or something similar). Be very careful of memory management here (i.e. see point 2!).

  4. The other thing you can do is handle things like 2D arrays defined as pointer to pointer (e.g. double**). See http://docs.cython.org/src/userguide/memoryviews.html#specifying-more-general-memory-layouts. I generally don't like this type of array, but if you have existing C code that already uses if then you can interface with that (and pass it back to Python so your Python code can also use it).


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接