Cython:内存视图的大小属性
Cython: size attribute of memoryviews
我在 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] = ...
如果我不关心索引 i
、j
和 k
,做一个平面循环会更有效率,比如
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, meaning that the unwieldy C code generated by a.size
cannot be optimized away. It looks to me as though the size
"attribute" is not really a pre-computed attribute, but actually carries out a computation upon lookup. Furthermore, this computation is quite a bit more involved than simply taking the product over the shape
attribute. I cannot find any hint of an explanation in the docs启用了所有可能的优化。
这个行为的解释是什么,如果我真的关心这个微优化,我有比写出 a.shape[0]*a.shape[1]*a.shape[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
性能几乎相同。
product 方法是纯 C 代码,如果它需要并行执行则很重要,否则与内存视图相比没有性能优势 size
。
通过查看生成的 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
并不总是 needed/used,并且人们不想为此付费。显然,如果你经常使用 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;
这意味着,Cython 代码可以非常有效地访问 shape
,因为它是一个简单的 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 对象 - 它是一种方便的包装器,它缓存重要数据,如 shape
和 stride
所以这个信息可以快速且便宜地访问。
当我们调用 a.size
时会发生什么?首先,调用 __pyx_memoryview_fromslice
,它会执行一些额外的引用计数和一些其他操作,并且 returns 来自 __Pyx_memviewslice
-object.
的成员 memview
然后 属性 size
在这个返回的 memoryview 上调用,它访问 _size
中的缓存值,如上面的 Cython 代码所示。
看起来 python 程序员为 shape
、strides
和 suboffsets
等重要信息引入了快捷方式,但 [=17] 却没有=] 这可能不是那么重要 - 这是在 shape
.
的情况下更清晰的 C 代码的原因
我在 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] = ...
如果我不关心索引 i
、j
和 k
,做一个平面循环会更有效率,比如
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, meaning that the unwieldy C code generated by a.size
cannot be optimized away. It looks to me as though the size
"attribute" is not really a pre-computed attribute, but actually carries out a computation upon lookup. Furthermore, this computation is quite a bit more involved than simply taking the product over the shape
attribute. I cannot find any hint of an explanation in the docs启用了所有可能的优化。
这个行为的解释是什么,如果我真的关心这个微优化,我有比写出 a.shape[0]*a.shape[1]*a.shape[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
性能几乎相同。
product 方法是纯 C 代码,如果它需要并行执行则很重要,否则与内存视图相比没有性能优势 size
。
通过查看生成的 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
并不总是 needed/used,并且人们不想为此付费。显然,如果你经常使用 size
,那么这种懒惰是要付出代价的——这就是 cython 所做的权衡。
我不会在调用 a.size
的开销上停留太久——与从 python.
例如,@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;
这意味着,Cython 代码可以非常有效地访问 shape
,因为它是一个简单的 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 对象 - 它是一种方便的包装器,它缓存重要数据,如 shape
和 stride
所以这个信息可以快速且便宜地访问。
当我们调用 a.size
时会发生什么?首先,调用 __pyx_memoryview_fromslice
,它会执行一些额外的引用计数和一些其他操作,并且 returns 来自 __Pyx_memviewslice
-object.
memview
然后 属性 size
在这个返回的 memoryview 上调用,它访问 _size
中的缓存值,如上面的 Cython 代码所示。
看起来 python 程序员为 shape
、strides
和 suboffsets
等重要信息引入了快捷方式,但 [=17] 却没有=] 这可能不是那么重要 - 这是在 shape
.