Cython:memoryviews的size属性

时间:2018-04-19 11:18:07

标签: python attributes cython memoryview typed-memory-views

我在Cython中使用了很多3D内存视图,例如

cython.declare(a='double[:, :, ::1]')
a = np.empty((10, 20, 30), dtype='double')

我经常想要遍历a的所有元素。我可以使用像

这样的三重循环来做到这一点
for i in range(a.shape[0]):
    for j in range(a.shape[1]):
        for k in range(a.shape[2]):
            a[i, j, k] = ...

如果我不关心索引ijk,那么执行扁平循环会更有效,例如

cython.declare(a_ptr='double*')
a_ptr = cython.address(a[0, 0, 0])
for i in range(size):
    a_ptr[i] = ...

这里我需要知道数组中元素的数量(size)。这由shape属性中的元素的乘积给出,即size = a.shape[0]*a.shape[1]*a.shape[2],或更一般地size = np.prod(np.asarray(a).shape)。我发现这些都很难写,而且(虽然很小)的计算开销困扰着我。这样做的好方法是使用内存视图的内置size属性size = a.size。但是,由于我无法理解的原因,这导致未经优化的C代码,从Cython生成的注释html文件中可以看出。具体来说,size = a.shape[0]*a.shape[1]*a.shape[2]生成的C代码只是

__pyx_v_size = (((__pyx_v_a.shape[0]) * (__pyx_v_a.shape[1])) * (__pyx_v_a.shape[2]));

size = a.size生成的C代码是

__pyx_t_10 = __pyx_memoryview_fromslice(__pyx_v_a, 3, (PyObject *(*)(char *)) __pyx_memview_get_double, (int (*)(char *, PyObject *)) __pyx_memview_set_double, 0);; if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 2238, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_10);
__pyx_t_14 = __Pyx_PyObject_GetAttrStr(__pyx_t_10, __pyx_n_s_size); if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 2238, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_14);
__Pyx_DECREF(__pyx_t_10); __pyx_t_10 = 0;
__pyx_t_7 = __Pyx_PyIndex_AsSsize_t(__pyx_t_14); if (unlikely((__pyx_t_7 == (Py_ssize_t)-1) && PyErr_Occurred())) __PYX_ERR(0, 2238, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_14); __pyx_t_14 = 0;
__pyx_v_size = __pyx_t_7;

为了生成上面的代码,我已经通过compiler directives启用了所有可能的优化,这意味着a.size生成的难以处理的C代码无法进行优化。它看起来好像是size"属性"实际上并不是预先计算的属性,但实际上是在查找时执行计算。此外,这个计算比简单地将产品放在shape属性上要多得多。我在docs中找不到任何解释的提示。

这种行为有什么解释,如果我真的关心这种微观优化,我还有比写出a.shape[0]*a.shape[1]*a.shape[2]更好的选择吗?

2 个答案:

答案 0 :(得分:3)

通过查看生成的C代码,您已经可以看到size是属性而不是简单的C成员。这是original Cython-code for memory-views

@cname('__pyx_memoryview')
cdef class memoryview(object):
...
   cdef object _size
...
    @property
    def size(self):
        if self._size is None:
            result = 1

            for length in self.view.shape[:self.view.ndim]:
                result *= length

            self._size = result

return self._size

很容易看出,产品只计算一次然后缓存。显然,它对于三维数组并没有起到重要作用,但是对于更高维度的数据,缓存可能变得非常重要(正如我们将要看到的,最多只有8个维度,所以它不是那么明确,这个缓存非常值得。)

人们可以理解懒洋洋地计算size的决定 - 毕竟,size并不总是需要/使用,并且人们并不想为此付出代价。显然,如果你经常使用size,那么这种懒惰是有代价的 - 这就是cython所做的权衡。

我不会在调用a.size的开销上花费太长时间 - 与从python调用cython函数的开销相比,这没什么。

例如,@ danny的测量仅测量此python调用开销,而不测量不同方法的实际性能。为了表明这一点,我将第三个函数放入混合中:

%%cython
...
def both():
    a.size+a.shape[0]*a.shape[1]*a.shape[2]

做了两倍的工作,但是

>>> %timeit mv_size
22.5 ns ± 0.0864 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

>>> %timeit mv_product
20.7 ns ± 0.087 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

>>>%timeit both
21 ns ± 0.39 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

同样快。另一方面:

%%cython
...
def nothing():
   pass

速度更快:

%timeit nothing
24.3 ns ± 0.854 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

简而言之:我会使用a.size因为可读性,假设优化不会加速我的应用程序,除非分析证明不同的东西。

整个故事:变量a的类型为__Pyx_memviewslice,而不是类型__pyx_memoryview。结构__Pyx_memviewslice具有以下定义:

struct __pyx_memoryview_obj;
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;

这意味着,shape可以通过Cython代码非常有效地访问,因为它是一个简单的C数组(顺便说一句。我问自己,如果有超过8个维度会发生什么? - 答案是:你不能超过8个维度)。

成员memview是内存所在的位置,__pyx_memoryview_obj是C-Extension,它是从我们上面看到的cython代码生成的,如下所示:

/* "View.MemoryView":328
 * 
 * @cname('__pyx_memoryview')
 * cdef class memoryview(object):             # <<<<<<<<<<<<<<
 * 
 *     cdef object obj
 */
struct __pyx_memoryview_obj {
  PyObject_HEAD
  struct __pyx_vtabstruct_memoryview *__pyx_vtab;
  PyObject *obj;
  PyObject *_size;
  PyObject *_array_interface;
  PyThread_type_lock lock;
  __pyx_atomic_int acquisition_count[2];
  __pyx_atomic_int *acquisition_count_aligned_p;
  Py_buffer view;
  int flags;
  int dtype_is_object;
  __Pyx_TypeInfo *typeinfo;
};

所以,Pyx_memviewslice实际上并不是一个Python对象 - 它是一种方便的包装器,可以缓存重要数据,如shapestride,因此可以快速,便宜地访问这些信息

当我们致电a.size时会发生什么?首先,调用__pyx_memoryview_fromslice,它会执行一些额外的引用计数和一些其他内容,并从memview - 对象返回成员__Pyx_memviewslice

然后在这个返回的内存视图上调用属性size,它访问_size中的缓存值,如上面的Cython代码中所示。

好像python程序员为shapestridessuboffsets这样的重要信息引入了快捷方式,但对于size可能不是非常重要 - 这就是shape

中更清晰的C代码的原因

答案 1 :(得分:2)

a.size生成的C代码看起来很好。

它必须与Python接口,因为内存视图是python扩展类型。内存视图上的size是一个python属性,并转换为ssize_t。这就是所有的C代码。通过将size变量键入Py_ssize_t而不是ssize_t,可以避免转换。

因此,C代码中没有任何东西看起来没有被优化 - 它只是在python对象上查找属性,在​​这种情况下是内存视图上的大小。

以下是这两种方法的微基准测试结果。

设定:

cimport numpy as np
import numpy as np
cimport cython
cython.declare(a='double[:, :, ::1]')
a = np.empty((10, 20, 30), dtype='double')

def mv_size():
    return a.size
def mv_product():
    return a.shape[0]*a.shape[1]*a.shape[2]

结果:

%timeit mv_size
10000000 loops, best of 3: 23.4 ns per loop

%timeit mv_product
10000000 loops, best of 3: 23.4 ns per loop

表现非常相似。

产品方法纯粹是C代码,如果需要并行执行则很重要,但除了内存视图size之外没有性能优势。