簡體   English   中英

Cython:memoryviews的size屬性

[英]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] = ...

如果我不在乎索引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) 我發現這些都很難編寫,並且(盡管很小)計算開銷使我感到困擾。 做到這一點的好方法是使用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對象-它是一種方便包裝程序,可以緩存重要數據(例如shapestride以便可以快速,廉價地訪問此信息。

當我們調用a.size時會發生什么? 首先, __pyx_memoryview_fromslice ,它執行一些附加的引用計數和其他操作,並從__Pyx_memviewslice返回成員memview

然后,將屬性size上調用該返回memoryview,它訪問在緩存值_size如已經在用Cython代碼以上已經示出。

看起來python程序員似乎為諸如shapestrides和子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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM