型付きメモリビュー (Typed Memoryview)

型つきメモリビューを使うと、 NumPy のアレイのようなバッファに、 Python のオーバヘッドを伴わずに効率的にアクセスできます。メモリビューは現在の numpy のアレイバッファサポート (np.ndarray[np.float64_t, ndim=2]) に似ていますが、より多くの機能を備え、よりクリーンな構文で書けます。

メモリビューは以前の numpy アレイバッファサポートより高い汎用性を持っ ています。というのも、メモリビューは広範なアレイデータソースを扱えるか らです。例えば、 C のアレイや Cython のアレイ型 (Cython のアレイ) などを扱えます。

メモリビューはあらゆるコンテキスト (関数パラメタ、モジュールレベル変数、 cdef クラスのアトリビュートなど) で利用でき、 PEP 3118 バッファイン タフェースを備える、書き込み可能なバッファを公開している全てのオブジェ クトに対して作成できます。

クイックスタート

# Cython アレイを作成するために cython.vew.array を import する
from cython.view cimport array as cvarray

import numpy as np

# numpy アレイを作成
narr = np.arange(27).reshape((3,3,3))

# numpy アレイの値を丸めるメモリビュー
cdef int [:, :, :] narr_view = narr

# C のアレイ
cdef int carr[3][3][3]

# C アレイの値を丸めるメモリビュー
cdef int [:, :, :] carr_view = carr

# Cython のアレイ
cyarr = cvarray(shape=(3, 3, 3), itemsize=sizeof(int), format="i")

# Cython アレイの値を丸めるメモリビュー
cdef int [:, :, :] cyarr_view = cyarr

# 変更前に、アレイ全体の総和を取る
print "Numpy sum of the Numpy array before assignments:", narr.sum()

# 以下のように、 C アレイなどに、他のメモリビューから値をセットできます

# Ellipsis を使って代入
carr_view[...] = narr_view
# コロンを使って代入
cyarr_view[:] = narr_view
# 複数コロンを使ってメモリブロック全体に代入
narr_view[:, :, :] = 3

# アレイを区別するために別の値を入れる
carr_view[0, 0, 0] = 100
cyarr_view[0, 0, 0] = 1000

# numpy アレイのメモリビューを変更すると、 numpy アレイの内容も
# インプレースで変更される
print "Numpy sum of Numpy array after assignments:", narr.sum()

# 通常、メモリビューを使う関数には GIL は不要
cpdef int sum3d(int[:, :, :] arr) nogil:
    cdef int total = 0
    I = arr.shape[0]
    J = arr.shape[1]
    K = arr.shape[2]
    for i in range(I):
        for j in range(J):
            for k in range(K):
                total += arr[i, j, k]
    return total

# メモリビューを渡せる関数は、 numpy アレイも扱える
print "Memoryview sum of Numpy array is", sum3d(narr)

# C アレイも
print "Memoryview sum of C array is", sum3d(carr)

# Cython アレイも
print "Memoryview sum of Cython array is", sum3d(cyarr)

# もちろん、メモリビューも
print "Memoryview sum of C memoryview is", sum3d(carr_view)

上のコードを実行すると、以下のような出力結果になります:

Numpy sum of the Numpy array before assignments: 351
Numpy sum of Numpy array after assignments: 81
Memoryview sum of Numpy array is 81
Memoryview sum of C array is 451
Memoryview sum of Cython array is 1351
Memoryview sum of C memoryview is 451

メモリビューを使う

インデクシングとスライス

インデクシングやスライスは、GIL があってもなくてもできます。操作は numpy と同様です。インデクスを全ディメンジョンに指定すれば、アレイのベー スタイプ (例えば int) の要素を得ます。それ以外の指定方法では、新たな ビューを得ます。 Ellipsis を使うと、指定のないディメンジョンについては すべて連続となるようなスライスを得ます。:

cdef int[:, :, :] my_view = ...

# These are all equivalent
my_view[10]
my_view[10, :, :]
my_view[10, ...]

コピー

メモリビュー上の値はインプレースでコピーできます:

cdef int[:, :, :] to_view, from_view
...

# copy the elements in from_view to to_view
to_view[...] = from_view
# or
to_view[:] = from_view
# or
to_view[:, :, :] = from_view

copy()copy_fortran() といったメソッドでもコピーできます。 詳しくは C-連続アレイと Fortran-連続アレイのコピー を参照してください。

転置

大抵の場合 (詳しくは後述)、 メモリビューは numpy スライスの転置と同じ ように転置できます:

cdef int[:, ::1] c_contig = ...
cdef int[::1, :] f_contig = c_contig.T

この操作は、新たな転置済みのデータビューを返します。

転置を行うには、メモリビューの全ディメンジョンが直接アクセス可能なメモ リレイアウト (ポインタを介した間接アクセスでない) でなければなりません。 詳しくは より汎用的なメモリレイアウト指定 を参照してください。

軸の追加 (newaxis)

numpy と同様、アレイを None でインデクシングすると、新たな軸を追加 できます:

cdef double[:] myslice = np.linspace(0, 10, num=50)

# 2D array with shape (1, 50)
myslice[None] # or
myslice[None, :]

# 2D array with shape (50, 1)
myslice[:, None]

新たに追加した軸に対するインデクシングは、他のインデクシングやスライス と混在させられます。 example を参照してください。

古い buffer サポートとの比較

メモリビューは、古い buffer オブジェクト操作の構文に比べて、以下の点で 優れています:

  • 構文がクリーンである
  • 通常、メモリビューの操作には GIL が不要 (メモリビューと GIL 参照)
  • メモリビューの方がかなり高速である

例えば、以下は前述の sum3d 関数と同じものを、古い構文で書いたもの です:

cpdef int old_sum3d(object[int, ndim=3, mode='strided'] arr):
    cdef int I, J, K, total = 0
    I = arr.shape[0]
    J = arr.shape[1]
    K = arr.shape[2]
    for i in range(I):
        for j in range(J):
            for k in range(K):
                total += arr[i, j, k]
    return total

sum3d の時と違って、この関数で使っている buffer には nogil を 使えません。 buffer が Python のオブジェクトだからです。しかし、仮に メモリビュー版で nogil を使わなかったとしても、メモリビュー版の方 が格段に高速です。実際に IPython セッション上でそれぞれの関数を import して比較すると、以下のようになります:

In [2]: import numpy as np

In [3]: arr = np.zeros((40, 40, 40), dtype=int)

In [4]: timeit -r15 old_sum3d(arr)
1000 loops, best of 15: 298 us per loop

In [5]: timeit -r15 sum3d(arr)
1000 loops, best of 15: 219 us per loop

Python の buffer サポート

Cython のメモリビューは、 Python の 新しい形式の buffer インタフェー スを備えるオブジェクトのほとんどをサポートします。 新しい buffer イン タフェースは PEP 3118 で解説しています。 Cython のアレイ で実演しているように、numpy アレイはこのインタフェースをサポートしてい ます。「ほとんどをサポートしている」とは、Python の buffer インタフェー スがデータアレイ中の 要素 をポインタにできるからです。 Cython のメモ リビューは、まだポインタの buffer をサポートしていません。

メモリレイアウト

buffer インタフェースを使うと、オブジェクトの背後にあるメモリを様々な 方法で識別できます。データ要素へのポインタからなるバッファを除き、 Cython のメモリビューは Python の新しい buffer のメモリレイアウトを全 てサポートしています。メモリが外部ルーチンの都合やコード最適化のために 特定のフォーマットになっている場合、メモリレイアウトがどうなっているか を知っていたり、レイアウトを指定できると便利です。

背景

メモリレイアウトは、データアクセス (data access) とデータパック (data packing) という概念からなります。データアクセスは、直接アクセス (ポインタを使わない) と間接アクセス (ポインタを使う) のいずれかです。 データパックは、データがメモリ上で連続かあるいは非連続か、あるいは、 各ディメンジョンでとなり合うインデクス間を移動するときのメモリ上の飛び 幅である ストライド(stride) の使い方です。

numpy アレイはストライドを使う直接データアクセスの良い例です。そこで、 numpy アレイを使って、 C や Fortran の連続アレイの概念、データストライ ドの概念を思い出してみましょう。

C, Fortran, ストライドメモリレイアウトの簡単なおさらい

最も単純なデータレイアウトは、 C の連続アレイ (C-連続: C contiguous) です。このレイアウトは、numpy や Cython のアレイのデフォルトのレイアウ トです。C-連続は、アレイデータがメモリ上で連続に配置されており (下記参 照)、アレイの第一ディメンジョンの近傍の要素がメモリ上では最も離れてい て、最終ディメンジョンの近傍の要素が最も近いレイアウトです。例えば、 numpy では:

In [2]: arr = np.array([['0', '1', '2'], ['3', '4', '5']], dtype='S1')

arr[0, 0]arr[0, 1] はメモリ上では 1 バイトしか離れていませ んが、 arr[0, 0]arr[1, 0] は 3 バイト離れています。 ここから ストライド という考え方にたどり着きます。アレイの各軸には、 軸上のある要素から隣の要素まで移動するのに必要なバイト数というものがあ ります。これがすなわちストライド長 (stride length) です。上の例では、 第 0 軸と第 1 軸のストライドは、それぞれ以下のようになることが分かりま す:

In [3]: arr.strides
Out[4]: (3, 1)

3D の C-連続アレイの場合は以下のとおりです:

In [5]: c_contig = np.arange(24, dtype=np.int8).reshape((2,3,4))
In [6] c_contig.strides
Out[6]: (12, 4, 1)

Fortran-連続 (Fortran contiguous) アレイは逆のメモリ配置で、最初の軸の 要素が、メモリ上でも最近傍で隣接します:

In [7]: f_contig = np.array(c_contig, order='F')
In [8]: np.all(f_contig == c_contig)
Out[8]: True
In [9]: f_contig.strides
Out[9]: (1, 2, 6)

連続アレイとは、単一の連続なメモリブロックに、アレイの全要素のデータが 入っているようなアレイです。従って、連続アレイのメモリブロック長は、ア レイの要素数と各要素のバイト長の積です。上の例では、メモリブロックの大 きさは 2 * 3 * 4 * 1 バイトです。最後の 1 は、 int8 の大きさです。

アレイは、 C や Fortran の並び順でなくても連続であり得ます:

In [10]: c_contig.transpose((1, 0, 2)).strides
Out[10]: (4, 12, 1)

numpy アレイをスライスすると、連続性は簡単になくなります:

In [11]: sliced = c_contig[:,1,:]
In [12]: sliced.strides
Out[12]: (12, 1)
In [13]: sliced.flags
Out[13]:
C_CONTIGUOUS : False
F_CONTIGUOUS : False
OWNDATA : False
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False

メモリビューレイアウトのデフォルトの挙動

より汎用的なメモリレイアウト指定 でも触れたように、メモリビューのどのディメ ンジョンに対してもメモリレイアウトを指定できます。レイアウトを指定しな いディメンジョンの場合、データアクセスは直接、データパックはストライド 型になります。例えば、以下のようなメモリビューは、直接アクセス、ストラ イド型です:

int [:, :, :] my_memoryview = obj

C-連続および Fortran-連続のメモリビュー

宣言時に ::1 ステップ構文を使って、メモリビューのレイアウトを C-連続や Fortran-連続にできます。例えば、メモリビューを 3D の C-連続レ イアウトにしたければ、以下のように書きます:

cdef int[:, :, ::1] c_contiguous = c_contig

ここで c_contig は C-連続の numpy アレイです。三番目に ::1 を 置くと、第三ディメンジョンがメモリ上で要素の隣接するディメンジョンにな ります。 3D の Fortran-連続のアレイを作りたければ、以下のように書きま す:

cdef int[::1, :, :] f_contiguous = f_contig

以下のようなことを試みると:

# アレイは C-連続
c_contig = np.arange(24).reshape((2,3,4))
cdef int[:, :, ::1] c_contiguous = c_contig

# こんなことはできない
c_contiguous = np.array(c_contig, order='F')

実行時に下記のような ValueError を引き起こします:

/Users/mb312/dev_trees/minimal-cython/mincy.pyx in init mincy (mincy.c:17267)()
    69
    70 # こんなことはできない
---> 71 c_contiguous = np.array(c_contig, order='F')
    72

/Users/mb312/dev_trees/minimal-cython/stringsource in View.MemoryView.memoryview_cwrapper (mincy.c:9995)()

/Users/mb312/dev_trees/minimal-cython/stringsource in View.MemoryView.memoryview.__cinit__ (mincy.c:6799)()

ValueError: ndarray is not C-contiguous

このように、スライス形式の型指定で ::1 を使うと、データがどのディメ ンジョンで連続かを指定できます。 ::1 は、完全な C-連続か Fortran連続 かの指定しかできません。

C-連続アレイと Fortran-連続アレイのコピー

C-連続や Fortran-連続のアレイは .copy().copy_fortran() メ ソッドを使ってコピーできます:

# コピー先のメモリビューが C 連続の場合
cdef int[:, :, ::1] c_contiguous = myview.copy()

# コピー先のメモリビューが Fortran 連続の場合
cdef int[::1, :] f_contiguous_slice = myview.copy_fortran()

より汎用的なメモリレイアウト指定

データレイアウトは、先の節で触れた ::1 を使ったスライス構文の他に、 cython.view で定義している定数を使って指定します。どのディメンジョ ンにもレイアウトの指定がなければ、データアクセスは直接アクセス、データ パックはストライド型とみなします。あるディメンジョンが直接アクセスなの か、間接アクセスなのかが分からない場合 (仕様未知のライブラリからバッファ インタフェースを備えたオブジェクトを受け取った場合) は、 generic フ ラグを指定できます。 generic にすると、レイアウトは実行時に決定しま す。

フラグは以下の通りです:

  • generic - ストライド型で、直接あるいは間接アクセス
  • strided - ストライド型で、直接アクセス (これがデフォルト)
  • indirect - ストライド型で、間接アクセス
  • contiguous - 連続で、直接アクセス
  • indirect_contiguous - 連続のポインタリスト

レイアウト指定の定数は、以下のように使います:

from cython cimport view

# どちらのディメンジョンも直接アクセスで、最初のディメンジョンがス
# トライド型、後のディメンジョンが連続型

cdef int[:, ::view.contiguous] a

# int の連続型リストへのポイントからなる連続型リスト
cdef int[::view.indirect_contiguous, ::1] b

# 最初のディメンジョンは直接または間接アクセス、二つ目のディメンジョ
# ンは直接アクセス。どちらもストライド型
cdef int[::view.generic, :] c

連続型に指定できるのは、最初と最後のディメンジョンの他、間接アクセスの ディメンジョンの後に続くディメンジョンだけです:

# これは無効
cdef int[::view.contiguous, ::view.indirect, :] a
cdef int[::1, ::view.indirect, :] b

# これは有効
cdef int[::view.indirect, ::1, :] a
cdef int[::view.indirect, :, ::1] b
cdef int[::view.indirect_contiguous, ::1, :]

contiguous フラグと ::1 の違いは、前者がディメンジョン一つだけを連 続型と指定しているのに対して、後者は後に続く全て (Fortran連続の場合) または前にある全て (C連続の場合) のディメンジョンを連続に指定するとこ ろです:

cdef int[:, ::1] c_contig = ...

# これは有効
cdef int[:, ::view.contiguous] myslice = c_contig[::2]

# これは無効
cdef int[:, ::1] myslice = c_contig[::2]

前者のケースが有効なのは、末尾のディメンジョンが連続型である一方、最初 のディメンジョンが末尾のディメンジョンの連続型に「引っ張られ」ないため です。 (このアレイはストライド型になっているが、C やFortran 連続ではな い) そのため、スライスが可能になっています。

メモリビューと GIL

クイックスタート の節で触れたように、メモリビューの操作にはしば しば GIL が不要です:

cpdef int sum3d(int[:, :, :] arr) nogil:
    ...

とりわけ、メモリビューのインデクシング、スライス、転置といった操作には GIL は不要です。メモリビューが GIL を必要とするのはコピーメソッド (C-連続アレイと Fortran-連続アレイのコピー 参照) を使うときや、 dtype がオブジェクト 型である場合で、オブジェクトの要素を読み書きする時です。

メモリビューオブジェクトと Cython のアレイ

型付きメモリビューは Python のメモリビューオブジェクト (cython.view.memoryview) に変換できます。Python のメモリビューオブジェ クトは、元のメモリビューと同じようにインデクシング・スライス・転置でき ます。メモリビューオブジェクトは、 Cython 空間のメモリビューとの間でい つでも相互変換できます。

Python のメモリビューには以下のアトリビュートがあります:

  • shape
  • strides
  • suboffsets
  • ndim
  • size
  • itemsize
  • nbytes
  • base

もちろん、前述の T アトリビュート (転置 参照) もあります。これらのアトリビュートは NumPy でのアトリビュートと同じセ マンティクスを備えます。例えば、元のオブジェクトを取り出すには:

import numpy
cimport numpy as cnp

cdef cnp.int32_t[:] a = numpy.arange(10, dtype=numpy.int32)
a = a[::2]

print a, numpy.asarray(a), a.base

# このコードは 「<MemoryView of 'ndarray' object> [0 2 4 6 8] [0 1 2 3 4 5 6 7 8 9] 」を出力する

のようにします。

上の例は、メモリビューを作成するときにもとにしたオブジェクトを返します。 一方、メモリビューは再スライスされています。

Cython のアレイ

Cython のメモリビューを (copycopy_fortran メソッドで)コピーす るたびに、新たな cython.view.array が生成され、そのメモリビュース ライスも新たに生成されます。生成されたアレイはデータブロックのメモリ領 域を自動で確保し、アレイ自体は手動で扱えます。また、アレイの内容を、後 で C/Fortran連続のスライス (またはストライド型スライス) に代入できます。 Cython のアレイは以下のように使います:

from cython cimport view

my_array = view.array(shape=(10, 2), itemsize=sizeof(int), format="i")
cdef int[:, :] my_slice = my_array

Cython のアレイのコンストラクタは、 mode (‘c’ または ‘fortran’) と、 allocate_buffer というオプションの引数を取ります。 allocate_buffer は、アレイの作成時にバッファのメモリ領域を確保し、スコープから出たとき に free するかどうかを決めます:

cdef view.array my_array = view.array(..., mode="fortran", allocate_buffer=False)
my_array.data = <char *> my_data_pointer

# (必要なら) データのデアロケートを行える関数を定義しておく
my_array.callback_free_data = free

ポインタをアレイにキャストしたり、 C アレイを Cython アレイにキャスト したりできます:

cdef view.array my_array = <int[:10, :2]> my_data_pointer
cdef view.array my_array = <int[:, :]> my_c_array

もちろん、 cython.view.array を直接型付きメモリビューに代入するの も可能です。 C アレイは、メモリビュースライスに直接代入できます:

cdef int[:, ::1] myslice = my_2d_c_array

Cython アレイはメモリビューと同じ操作で Python 側でインデクシングした りスライスしたりでき、メモリビューオブジェクトと同じアトリビュートを備 えています。

CPython のアレイモジュール

cython.view.array の代用になるのが、 Python 標準ライブラリの array モジュールです。 Python 3 では、 array.array 型はネイティ ブで buffer インタフェースをサポートしているので、メモリビューは特に準 備しなくても array.array 型の上に作成できます。

ただし、 Cython 0.17 からは、 Python 2 でも array.array をバッファ の供給元として使えます。そのためには、以下のように cpython.array モジュールを明示的に cimport します:

cimport cpython.array

def sum_array(int[:] view):
    """
    >>> from array import array
    >>> sum_array( array('i', [1,2,3]) )
    6
    """
    cdef int total
    for i in range(view.shape[0]):
        total += view[i]
    return total

cimport はアレイ型に対して古いバッファ構文も使えるようにします。従って、 以下のコードも動きます:

from cpython cimport array

def sum_array(array.array[int] arr):  # 古いバッファ構文を使用
    ...

numpy データ型への型強制

メモリビュー (と、アレイ) オブジェクトは numpy の ndarray 型に型強制で きます。その際、データをコピーする必要はありません。例えば、以下のよう に操作できます:

cimport numpy as np
import numpy as np

numpy_array = np.asarray(<np.int32_t[:10, :10]> my_pointer)

もちろん、型強制の際は (np.int32_t のような) numpy の型に限らず、 利用可能な型は何でも使えます。

None スライス

メモリビューのスライスは Python のオブジェクトではありませんが、値を None にセットでき、 None であるかどうかの判定も可能です:

def func(double[:] myarray = None):
    print myarray is None

関数の入力値を実際のメモリビューにする必要があるなら、シグネチャで明に None をはじくのがベストです。これは Cython 0.17 以降で導入された機能で す:

def func(double[:] myarray not None):
    ...

拡張型クラスのオブジェクトアトリビュートと違い、メモリビューのスライス は None で初期化しません。