python - Cython:内存 View 的大小属性

标签 python attributes cython memoryview typed-memory-views

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

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)。我发现这两个都很难写,而且(尽管很小)计算开销困扰着我。最好的方法是使用内存 View 的内置 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] 更好的选择,如果我真的很关心这个微优化?

最佳答案

通过查看生成的 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

很容易看出,产品只计算一次然后缓存。显然它对 3 维数组没有太大作用,但对于更高维数的缓存可能变得非常重要(正如我们将看到的,最多有 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,它会执行一些额外的引用计数和一些其他操作,并从 __Pyx_memviewslice 对象返回成员 memview

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

看起来好像 python 程序员为 shapestridessuboffsets 等重要信息引入了快捷方式,但不是为size 这可能不是那么重要 - 这就是在 shape 的情况下 C 代码更清晰的原因。

关于python - Cython:内存 View 的大小属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49919768/

相关文章:

Cython:使其他 Cython 模块可以访问外部 C 函数

python - 用户更改密码后退出

python - 将 Pandas 时区感知 DateTimeIndex 转换为天真的时间戳,但在特定时区

python - 使用通过python连接的表单在数据库中插入数据

.net - 在 .Net 中,为什么调用 Type.GetCustomAttributes(true) 时不返回接口(interface)上声明的属性?

c# - 方法参数的属性

python - cython错误无法分配给外部并行 block 的私有(private)

python - 如何使用 javascript 检索的表格内容抓取网站?

c# - MVC3 范围属性 - 不允许值为零

python - 同一包中的 cython 导入错误