7

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。ただし、理解できない理由により、Cython によって生成された注釈 html ファイルから明らかなように、これは最適化されていない C コードにつながります。具体的には、によって生成される 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さらに、この計算は、単純に属性の積を取得するよりもかなり複雑です。docsに説明のヒントが見つかりません。

a.shape[0]*a.shape[1]*a.shape[2]この動作の説明は何ですか?このマイクロ最適化を本当に気にかけているのであれば、書くよりも良い選択がありますか?

4

2 に答える 2

6

生成された 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

積が 1 回だけ計算されてからキャッシュされることは簡単にわかります。3 次元配列では大きな役割を果たさないことは明らかですが、より多くの次元ではキャッシングが非常に重要になる可能性があります (後で説明するように、最大​​で 8 次元しかないため、このキャッシングが本当に価値があります)。

怠惰に計算するという決定を理解することができますsize- 結局、size常に必要/使用されるとは限らず、それにお金を払いたくないのです。明らかに、a lot を使用する場合、この怠惰に対して支払う代償がありsizeます。これが cython のトレードオフです。

呼び出しa.sizeのオーバーヘッドについてはあまり詳しく説明しません。Python から cython 関数を呼び出すオーバーヘッドに比べれば、何ものでもありません。

たとえば、@danny の測定では、この python 呼び出しのオーバーヘッドのみが測定され、さまざまなアプローチの実際のパフォーマンスは測定されません。これを示すために、3 番目の関数をミックスに投入します。

%%cython
...
def both():
    a.size+a.shape[0]*a.shape[1]*a.shape[2]

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;

つまりshape、単純な C 配列であるため、Cython コードから非常に効率的にアクセスできます (ところで、8 次元を超えるとどうなるでしょうか? - 答えは、次のとおりです。 8次元)。

メンバーmemviewはメモリが保持される場所であり、__pyx_memoryview_obj上記の cython コードから生成された C 拡張であり、次のようになります。

/* "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;
};

So, Pyx_memviewslice is not really a Python object -it is kind of convenience wrapper, which caches important data, like shape and stride so this information can be accessed fast and cheap.

What happens when we call a.size? First, __pyx_memoryview_fromslice is called which does some additional reference counting and some further stuff and returns the member memview from the __Pyx_memviewslice-object.

Then the property size is called on this returned memoryview, which accesses the cached value in _size as have been shown in the Cython code above.

python-programmers が、shape、などの重要な情報へのショートカットを導入したように見えますが、おそらくそれほど重要ではない.stridessuboffsetssizeshape

于 2018-04-19T16:30:05.943 に答える
2

生成された C コードは正常にa.size見えます。

メモリ ビューは Python 拡張タイプであるため、Python とのインターフェイスが必要です。sizeメモリ ビューの は python 属性であり、に変換されssize_tます。Cコードが行うことはそれだけです。size変数をPy_ssize_tではなくとして入力すると、変換を回避できますssize_t

したがって、C コードには最適化されていないように見えるものは何もありません。Python オブジェクトの属性、この場合はメモリ ビューのサイズを検索しているだけです。

以下は、2 つの方法のマイクロベンチマークの結果です。

設定:

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

于 2018-04-19T13:44:41.973 に答える