web-dev-qa-db-ja.com

Cython:型付きメモリビューはnumpy配列を型付けする最新の方法ですか?

Numpy配列をcdef関数に渡したいとしましょう。

cdef double mysum(double[:] arr):
    cdef int n = len(arr)
    cdef double result = 0

    for i in range(n):
        result = result + arr[i]

    return result

これは、numpy配列の入力を処理するための最新の方法ですか?この質問と比較してください: cython /配列のnumpyタイプ

次のことをしたい場合はどうすればよいですか?

cdef double[:] mydifference(int a, int b):
    cdef double[:] arr_a = np.arange(a)
    cdef double[:] arr_b = np.arange(b)

    return arr_a - arr_b

-がメモリビューに定義されていないため、これはエラーを返します。それで、その場合は次のように扱われるべきでしたか?

cdef double[:] mydifference(int a, int b):
    arr_a = np.arange(a)
    arr_b = np.arange(b)

    return arr_a - arr_b
22
user89

ドキュメントから引用します ドキュメント

Memoryviewsは、現在のNumPy配列バッファーサポート(np.ndarray[np.float64_t, ndim=2])に似ていますが、より多くの機能とよりクリーンな構文を備えています。

これは、Cythonの開発者がメモリビューを最新の方法と見なしていることを示しています。

メモリビューは、主にエレガンスと相互運用性においてnp.ndarray表記に比べていくつかの大きな利点を提供しますが、パフォーマンスは優れていません。

パフォーマンス:

まず、boundscheck時々がメモリビューで機能せず、boundscheck = Trueのメモリビューの数値が人為的に高速になることに注意してください(つまり、高速で安全ではありません)インデックス作成)、バグをキャッチするためにboundscheckに依存している場合、これは厄介な驚きになる可能性があります。

ほとんどの場合、コンパイラの最適化が適用されると、メモリビューとnumpy配列表記のパフォーマンスは同じになり、多くの場合、正確に同じになります。違いがある場合、通常は10〜30%以下です。

パフォーマンスベンチマーク

数値は、1億回の操作を実行するための秒単位の時間です。小さいほど速くなります。

ACCESS+ASSIGNMENT on small array (10000 elements, 10000 times)
Results for `uint8`
1) memory view: 0.0415 +/- 0.0017
2) np.ndarray : 0.0531 +/- 0.0012
3) pointer    : 0.0333 +/- 0.0017

Results for `uint16`
1) memory view: 0.0479 +/- 0.0032
2) np.ndarray : 0.0480 +/- 0.0034
3) pointer    : 0.0329 +/- 0.0008

Results for `uint32`
1) memory view: 0.0499 +/- 0.0021
2) np.ndarray : 0.0413 +/- 0.0005
3) pointer    : 0.0332 +/- 0.0010

Results for `uint64`
1) memory view: 0.0489 +/- 0.0019
2) np.ndarray : 0.0417 +/- 0.0010
3) pointer    : 0.0353 +/- 0.0017

Results for `float32`
1) memory view: 0.0398 +/- 0.0027
2) np.ndarray : 0.0418 +/- 0.0019
3) pointer    : 0.0330 +/- 0.0006

Results for `float64`
1) memory view: 0.0439 +/- 0.0037
2) np.ndarray : 0.0422 +/- 0.0013
3) pointer    : 0.0353 +/- 0.0013

ACCESS PERFORMANCE (100,000,000 element array):
Results for `uint8`
1) memory view: 0.0576 +/- 0.0006
2) np.ndarray : 0.0570 +/- 0.0009
3) pointer    : 0.0061 +/- 0.0004

Results for `uint16`
1) memory view: 0.0806 +/- 0.0002
2) np.ndarray : 0.0882 +/- 0.0005
3) pointer    : 0.0121 +/- 0.0003

Results for `uint32`
1) memory view: 0.0572 +/- 0.0016
2) np.ndarray : 0.0571 +/- 0.0021
3) pointer    : 0.0248 +/- 0.0008

Results for `uint64`
1) memory view: 0.0618 +/- 0.0007
2) np.ndarray : 0.0621 +/- 0.0014
3) pointer    : 0.0481 +/- 0.0006

Results for `float32`
1) memory view: 0.0945 +/- 0.0013
2) np.ndarray : 0.0947 +/- 0.0018
3) pointer    : 0.0942 +/- 0.0020

Results for `float64`
1) memory view: 0.0981 +/- 0.0026
2) np.ndarray : 0.0982 +/- 0.0026
3) pointer    : 0.0968 +/- 0.0016

ASSIGNMENT PERFORMANCE (100,000,000 element array):
Results for `uint8`
1) memory view: 0.0341 +/- 0.0010
2) np.ndarray : 0.0476 +/- 0.0007
3) pointer    : 0.0402 +/- 0.0001

Results for `uint16`
1) memory view: 0.0368 +/- 0.0020
2) np.ndarray : 0.0368 +/- 0.0019
3) pointer    : 0.0279 +/- 0.0009

Results for `uint32`
1) memory view: 0.0429 +/- 0.0022
2) np.ndarray : 0.0427 +/- 0.0005
3) pointer    : 0.0418 +/- 0.0007

Results for `uint64`
1) memory view: 0.0833 +/- 0.0004
2) np.ndarray : 0.0835 +/- 0.0011
3) pointer    : 0.0832 +/- 0.0003

Results for `float32`
1) memory view: 0.0648 +/- 0.0061
2) np.ndarray : 0.0644 +/- 0.0044
3) pointer    : 0.0639 +/- 0.0005

Results for `float64`
1) memory view: 0.0854 +/- 0.0056
2) np.ndarray : 0.0849 +/- 0.0043
3) pointer    : 0.0847 +/- 0.0056

ベンチマークコード(アクセス+割り当ての場合にのみ表示)

# cython: boundscheck=False
# cython: wraparound=False
# cython: nonecheck=False
import numpy as np
cimport numpy as np
cimport cython

# Change these as desired.
data_type = np.uint64
ctypedef np.uint64_t data_type_t

cpdef test_memory_view(data_type_t [:] view):
    cdef Py_ssize_t i, j, n = view.shape[0]

    for j in range(0, n):
        for i in range(0, n):
            view[i] = view[j]

cpdef test_ndarray(np.ndarray[data_type_t, ndim=1] view):
    cdef Py_ssize_t i, j, n = view.shape[0]

    for j in range(0, n):
        for i in range(0, n):
            view[i] = view[j]

cpdef test_pointer(data_type_t [:] view):
    cdef Py_ssize_t i, j, n = view.shape[0]
    cdef data_type_t * data_ptr = &view[0]

    for j in range(0, n):
        for i in range(0, n):
            (data_ptr + i)[0] = (data_ptr + j)[0]

def run_test():
    import time
    from statistics import stdev, mean
    n = 10000
    repeats = 100
    a = np.arange(0, n,  dtype=data_type)
    funcs = [('1) memory view', test_memory_view),
        ('2) np.ndarray', test_ndarray),
        ('3) pointer', test_pointer)]

    results = {label: [] for label, func in funcs}
    for r in range(0, repeats):
        for label, func in funcs:
            start=time.time()
            func(a)
            results[label].append(time.time() - start)

    print('Results for `{}`'.format(data_type.__name__))
    for label, times in sorted(results.items()):
        print('{: <14}: {:.4f} +/- {:.4f}'.format(label, mean(times), stdev(times)))

これらのベンチマークは、全体としてパフォーマンスに大きな違いがないことを示しています。 np.ndarray表記が少し速い場合もあれば、その逆の場合もあります。

ベンチマークで注意すべきことの1つは、コードがもう少し複雑または「現実的」になると、コンパイラーが非常に巧妙な最適化を適用する自信を失ったかのように、違いが突然消えることです。これは、いくつかの派手な整数の最適化を使用できないため、おそらくまったく違いがないフロートのパフォーマンスで見ることができます。

使いやすさ

メモリビューには大きな利点があります。たとえば、現在と将来の両方で、numpy配列、CPython配列、cython配列、c配列などでメモリビューを使用できます。メモリビューに何かをキャストするための単純な並列構文もあります。

cdef double [:, :] data_view = <double[:256, :256]>data

メモリビューはこの点で優れています。メモリビューを取得する関数を入力すると、それらのいずれかを取得できるためです。これは、numpyに依存しないが、numpy配列を使用できるモジュールを作成できることを意味します。

一方、np.ndarray表記は、まだnumpy配列であるものになり、その上ですべてのnumpy配列メソッドを呼び出すことができます。ただし、numpy配列と配列のビューの両方を持つことは大したことではありません。

def dostuff(arr):
    cdef double [:] arr_view = arr
    # Now you can use 'arr' if you want array functions,
    # and arr_view if you want fast indexing

配列と配列ビューの両方を持つことは実際にはうまく機能し、Pythonレベルのメソッドとcレベルのメソッドを明確に区別するので、私はこのスタイルがとても好きです。

結論

パフォーマンスはほぼ同等であり、それが決定要因となるのに十分な違いは確かにありません。

Numpy配列表記は、フルスピードの配列インデックスを取得しながら同じ変数を引き続き使用できるため、コードをあまり変更せずにpythonコードを高速化するという理想に近づきます。

一方、メモリビュー表記はおそらく未来です。その優雅さが好きで、単にnumpy配列とは異なる種類のデータコンテナを使用する場合は、一貫性を保つためにメモリビューを使用するのには非常に理由があります。

38
Blake Walsh