我在 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
memoryviews 的内置属性,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]));
其中生成的 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;
为了生成上述代码,我通过编译器指令启用了所有可能的优化,这意味着无法优化生成的笨重的 C 代码a.size
。在我看来,size
“属性”并不是真正的预先计算的属性,而是在查找时实际执行计算。此外,这种计算比简单地用乘积代替shape
属性要复杂得多。我在文档中找不到任何解释的提示。
a.shape[0]*a.shape[1]*a.shape[2]
这种行为的解释是什么,如果我真的关心这个微优化,我有比写出更好的选择吗?