[英]Cython: size attribute of memoryviews
我在Cython中使用了很多3D memoryviews,例如
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)
。 我發現這些都很難編寫,並且(盡管很小)計算開銷使我感到困擾。 做到這一點的好方法是使用memoryviews的內置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;
為了生成上述代碼,我已經通過編譯器指令啟用了所有可能的優化,這意味着無法優化掉a.size
生成的笨拙的C代碼。 在我看來, size
“屬性”實際上並不是預先計算的屬性,但實際上是在查找時進行計算。 此外,此計算比簡單地將乘積超過shape
屬性要復雜得多。 我在文檔中找不到任何解釋的暗示。
對這種行為的解釋是什么?如果我真的很在乎這種微優化,那么我有比寫出a.shape[0]*a.shape[1]*a.shape[2]
更好的選擇嗎?
通過查看生成的C代碼,您已經可以看到size
是一個屬性,而不是簡單的C成員。 這是用於內存視圖的原始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
不難看出,乘積僅被計算一次,然后被緩存。 顯然,它對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.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擴展名,它是由我們上面看到的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
,它執行一些附加的引用計數和其他操作,並從__Pyx_memviewslice
返回成員memview
。
然后,將屬性size
上調用該返回memoryview,它訪問在緩存值_size
如已經在用Cython代碼以上已經示出。
看起來python程序員似乎為諸如shape
, strides
和子suboffsets
類的重要信息引入了快捷方式,但是對於可能並不那么重要的size
卻沒有提供快捷方式-這就是在shape
情況下使用更清晰的C代碼的原因。
為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
沒有性能優勢。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.