CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: NumPy 配列の操作でメモリのコピーが生じているか調べる

パフォーマンスの観点からいえば、データをコピーする機会は少ないほど望ましい。 コンピュータのバスの帯域幅は有限なので、データをコピーするには時間がかかる。

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 という情報があって、これはオブジェクトのメモリが自身のものか、それとも別のオブジェクトを参照しているかを表す。

numpy.org

最初に作った配列については、このフラグが True にセットされている。

>>> a.flags.owndata
True

では、上記に対してスライスを使って配列の一部を取り出した場合にはどうだろうか。

>>> b = a[1:]

スライスで取り出した配列の場合、フラグは False にセットされている。

>>> b.flags.owndata
False

つまり、自身でメモリを確保しているのではなく、別のオブジェクトを参照しているだけ。

ndarray#base を使った調べ方

同じように ndarray#base を使って調べることもできそうだ。 このアトリビュートは、オブジェクトが別のオブジェクトのメモリに由来している場合に、そのオブジェクトへの参照が入る。

numpy.org

先ほどの例では、最初に作った配列は None になっている。

>>> a.base is None
True

一方で、スライスを使って取り出した配列は、元になった配列への参照が入っている。

>>> b.base is a
True

インプレース演算

ところで、インプレース演算の場合は ndarray#flagsndarray#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.org

なお、それ以外にも大きな配列を用意してベンチマークしたり、ソースコードを読んで調べることも考えられる。