jmd*_*_dk 5 python attributes cython memoryview typed-memory-views
我在Cython中使用了很多3D内存视图,例如
cython.declare(a='double[:, :, ::1]')
a = np.empty((10, 20, 30), dtype='double')
Run Code Online (Sandbox Code Playgroud)
我经常想要遍历所有元素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] = ...
Run Code Online (Sandbox Code Playgroud)
如果我不关心指数i,j并且k,它是更有效地做一个扁平环形,像
cython.declare(a_ptr='double*')
a_ptr = cython.address(a[0, 0, 0])
for i in range(size):
a_ptr[i] = ...
Run Code Online (Sandbox Code Playgroud)
在这里,我需要知道size数组中的elements()数量.这由shape属性中的元素的乘积给出,即size = a.shape[0]*a.shape[1]*a.shape[2],或更一般地size = np.prod(np.asarray(a).shape).我发现这些都很难写,而且(虽然很小)的计算开销困扰着我.这样做的好方法是使用sizememoryviews 的builtin 属性size = a.size.但是,由于我无法理解的原因,这导致未经优化的C代码,从Cython生成的注释html文件中可以看出.具体来说,生成的C代码size = a.shape[0]*a.shape[1]*a.shape[2]很简单
__pyx_v_size = (((__pyx_v_a.shape[0]) * (__pyx_v_a.shape[1])) * (__pyx_v_a.shape[2]));
Run Code Online (Sandbox Code Playgroud)
生成C代码的地方size = a.size是
__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;
Run Code Online (Sandbox Code Playgroud)
为了生成上面的代码,我已经通过编译器指令启用了所有可能的优化,这意味着生成的笨重的C代码a.size无法进行优化.它看起来好像size"属性"实际上不是预先计算的属性,但实际上在查找时执行计算.此外,这个计算比简单地将产品放在shape属性上要多得多.我在文档中找不到任何解释的暗示.
这种行为有什么解释a.shape[0]*a.shape[1]*a.shape[2],如果我真的关心这种微优化,我还有比写出更好的选择吗?
通过查看生成的 C 代码,您已经可以看到它size是一个属性而不是一个简单的 C 成员。这是用于 memory-views的原始 Cython 代码:
@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
Run Code Online (Sandbox Code Playgroud)
很容易看出,乘积只计算一次然后缓存。显然,它对于 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]
Run Code Online (Sandbox Code Playgroud)
它做了两倍的工作,但是
>>> %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)
Run Code Online (Sandbox Code Playgroud)
一样快。另一方面:
%%cython
...
def nothing():
pass
Run Code Online (Sandbox Code Playgroud)
不是更快:
%timeit nothing
24.3 ns ± 0.854 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Run Code Online (Sandbox Code Playgroud)
简而言之:我会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;
Run Code Online (Sandbox Code Playgroud)
这意味着,shape可以通过 Cython 代码非常有效地访问,因为它是一个简单的 C 数组(顺便说一句。我问我自己,如果有超过 8 个维度会发生什么? - 答案是:你不能有超过8 个维度)。
成员memview是保存内存的地方,__pyx_memoryview_obj它是由我们上面看到的 cython 代码生成的 C-Extension,如下所示:
/* "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;
};
Run Code Online (Sandbox Code Playgroud)
那么,Pyx_memviewslice是不是一个真正的Python对象-它是一种方便的包装,其缓存重要数据,像shape和stride因此,这些信息可以快速和廉价的访问。
当我们打电话时会发生什么a.size?首先,__pyx_memoryview_fromslice调用它做一些额外的引用计数和一些进一步的事情,并memview从__Pyx_memviewslice-object返回成员。
然后size在这个返回的 memoryview 上调用该属性,它访问缓存的值,_size如上面的 Cython 代码所示。
看起来 python 程序员为诸如shape,strides和 之类的重要信息引入了一个快捷方式suboffsets,但对于size可能不那么重要的 则没有 - 这就是在shape.
| 归档时间: |
|
| 查看次数: |
410 次 |
| 最近记录: |