パフォーマンスの観点からいえば、データをコピーする機会は少ないほど望ましい。 コンピュータのバスの帯域幅は有限なので、データをコピーするには時間がかかる。
NumPy の配列 (ndarray) には、メモリを実際に確保している配列と、それをただ参照しているだけのビュー (view) がある。 そして、配列への操作によって、メモリが確保されて新しい配列が作られるか、それとも単なるビューになるかは異なる。 今回は NumPy の配列を操作するときにメモリのコピーが生じているか調べる方法について。
使った環境は次のとおり。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.5 BuildVersion: 19F101 $ python -V Python 3.7.7 $ pip list | grep -i numpy numpy 1.19.0
下準備
あらかじめ NumPy をインストールしておく。
$ pip install numpy
Python のインタプリタを起動する。
$ python
サンプルとなる配列を用意する。
>>> import numpy as np >>> a = np.arange(10)
flags を使った調べ方
はじめに ndarray#flags
を使った調べ方から。
NumPy の配列には flags
というアトリビュートがあって、ここから配列の情報がいくらか得られる。
この中に owndata
という情報があって、これはオブジェクトのメモリが自身のものか、それとも別のオブジェクトを参照しているかを表す。
最初に作った配列については、このフラグが True
にセットされている。
>>> a.flags.owndata
True
では、上記に対してスライスを使って配列の一部を取り出した場合にはどうだろうか。
>>> b = a[1:]
スライスで取り出した配列の場合、フラグは False
にセットされている。
>>> b.flags.owndata
False
つまり、自身でメモリを確保しているのではなく、別のオブジェクトを参照しているだけ。
ndarray#base を使った調べ方
同じように ndarray#base
を使って調べることもできそうだ。
このアトリビュートは、オブジェクトが別のオブジェクトのメモリに由来している場合に、そのオブジェクトへの参照が入る。
先ほどの例では、最初に作った配列は None
になっている。
>>> a.base is None True
一方で、スライスを使って取り出した配列は、元になった配列への参照が入っている。
>>> b.base is a True
インプレース演算
ところで、インプレース演算の場合は ndarray#flags
や ndarray#base
を使った判定ができないのかな、と思った。
たとえば配列を使った通常の加算 (__add__()
) では、新しく配列が作られてメモリのコピーが生じる。
>>> c = a + 1 >>> c.flags.owndata True >>> c.base is None True
一方で、インプレースの加算 (__iadd__()
) を使ったときも、これまで紹介してきたアトリビュートは同じ見え方になる。
>>> a += 1 >>> a.flags.owndata True >>> a.base is None True
では、メモリのコピーは生じているかというと生じていない。
NumPy の配列には __array_interface__
というアトリビュートがある。
その中にある data
というキーからは、配列の最初の要素が格納されているメモリのアドレス情報が得られる。
>>> a.__array_interface__['data'] (140489713031584, False)
以下のように、インプレース演算をしてもアドレス情報に変化はない。
>>> a += 1 >>> a.__array_interface__['data'] (140489713031584, False)
つまり、新たにメモリは確保されていない。
なお、それ以外にも大きな配列を用意してベンチマークしたり、ソースコードを読んで調べることも考えられる。
NumPyによるデータ分析入門 ―配列操作、線形代数、機械学習のためのPythonプログラミング
- 作者:Umit Mert Cakmak,Mert Cuhadaroglu
- 発売日: 2019/09/27
- メディア: 単行本(ソフトカバー)