ndarrayのfortran flagと実際のメモリ配列について

ndarrayのfortran flagと実際のメモリ配列について

要約

  • ndarrayで.Tで転置を行った場合、実際のメモリ上の配列は変更されず、.flagsで確認できるC形式配列(row-major)であるかFortran形式配列(column-major)であるかのフラグが入れ替わるのみ
  • np.saveで保存する場合も上記フラグは保たれたまま保存されるため、実際のレイアウトを転置させたい場合はndarrayに対し.copy()を読んだ上で保存する必要がある

実験

In [1]:
import numpy as np

2x2のndarrayを生成する。

In [2]:
a = np.array([[1,2], [3,4]])
a
Out[2]:
array([[1, 2],
       [3, 4]])

.Tで転置を取ると行と列が入れ替わる。

In [3]:
a.T
Out[3]:
array([[1, 3],
       [2, 4]])

元のndarrayの.flagsを表示すると、C_CONTIGUOUSTrueとなっている一方、F_CONTIGUOUSがFalseとなっていることが分かる。C_CONTIGUOUSはC形式配列であるかを表すフラグであり、F_CONTIGUOUSはFortran形式配列であるかを表すフラグである。

In [4]:
a.flags
Out[4]:
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False

一方.Tを呼んだ後のndarrayの.flagsを表示すると、C_CONTIGUOUSFalseに変化し、一方F_CONTIGUOUSがTrueとなっている。

In [5]:
a.T.flags
Out[5]:
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : False
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False

np.saveでの保存時の形式

.Tで転置を取ると.flagsのC形式配列であるかFortran形式配列であるかのフラグが入れ替わることが分かったが、実際の格納形式がどうなっているかを知るために、np.saveを用いて保存した.npyファイルに対して!hexdump -Cvを実行することでバイナリの中身を調べる。ここでhexdumpコマンドの-CはASCII表示を行うためのオプションであり、-vは全データを表示するためのオプションである(おそらくBSD系のhexdumpのみ)。

まず、np.saveで転置前後のndarrayを保存する。

In [6]:
np.save("a.npy", a)
np.save("a.T.npy", a.T)

これらをnp.loadで再度読み込み直すと、.flagsは読み込んだ後も保持されていることが分かる。

In [7]:
np.load("a.npy").flags
Out[7]:
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False
In [8]:
np.load("a.T.npy").flags
Out[8]:
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : False
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False

ディスクへダンプした際の実際の格納形式がどうなっているかをhexdumpコマンドを用いて調べる。

In [9]:
!hexdump -Cv a.npy
00000000  93 4e 55 4d 50 59 01 00  76 00 7b 27 64 65 73 63  |.NUMPY..v.{'desc|
00000010  72 27 3a 20 27 3c 69 38  27 2c 20 27 66 6f 72 74  |r': '<i8', 'fort|
00000020  72 61 6e 5f 6f 72 64 65  72 27 3a 20 46 61 6c 73  |ran_order': Fals|
00000030  65 2c 20 27 73 68 61 70  65 27 3a 20 28 32 2c 20  |e, 'shape': (2, |
00000040  32 29 2c 20 7d 20 20 20  20 20 20 20 20 20 20 20  |2), }           |
00000050  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |                |
00000060  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |                |
00000070  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 0a  |               .|
00000080  01 00 00 00 00 00 00 00  02 00 00 00 00 00 00 00  |................|
00000090  03 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|
000000a0
In [10]:
!hexdump -Cv a.T.npy
00000000  93 4e 55 4d 50 59 01 00  76 00 7b 27 64 65 73 63  |.NUMPY..v.{'desc|
00000010  72 27 3a 20 27 3c 69 38  27 2c 20 27 66 6f 72 74  |r': '<i8', 'fort|
00000020  72 61 6e 5f 6f 72 64 65  72 27 3a 20 54 72 75 65  |ran_order': True|
00000030  2c 20 27 73 68 61 70 65  27 3a 20 28 32 2c 20 32  |, 'shape': (2, 2|
00000040  29 2c 20 7d 20 20 20 20  20 20 20 20 20 20 20 20  |), }            |
00000050  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |                |
00000060  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |                |
00000070  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 0a  |               .|
00000080  01 00 00 00 00 00 00 00  02 00 00 00 00 00 00 00  |................|
00000090  03 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|
000000a0

ヘッダーのfortran_orderが変化していることが分かる。更にbashのプロセス置換で両者のdiffを見てみる。

In [11]:
%%bash
diff <(hexdump -Cv a.npy) <(hexdump -Cv a.T.npy)
3,5c3,5
< 00000020  72 61 6e 5f 6f 72 64 65  72 27 3a 20 46 61 6c 73  |ran_order': Fals|
< 00000030  65 2c 20 27 73 68 61 70  65 27 3a 20 28 32 2c 20  |e, 'shape': (2, |
< 00000040  32 29 2c 20 7d 20 20 20  20 20 20 20 20 20 20 20  |2), }           |
---
> 00000020  72 61 6e 5f 6f 72 64 65  72 27 3a 20 54 72 75 65  |ran_order': True|
> 00000030  2c 20 27 73 68 61 70 65  27 3a 20 28 32 2c 20 32  |, 'shape': (2, 2|
> 00000040  29 2c 20 7d 20 20 20 20  20 20 20 20 20 20 20 20  |), }            |

ヘッダーのみ差分が存在し、実際のデータが格納されている後半部は変化していないことが分かる。

転置した状態のまま保存する方法

ndarrayに対して.copy()を呼んだ上で保存すれば良い。下記に実例を示す。 まず、.copy()を呼んだ後の.flagsを表示する。

In [12]:
a.T.copy().flags
Out[12]:
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False

転置したにもかかわらずC_CONTIGUOUSがTrueになっていることが分かる。 更にこのndarrayをnp.saveで保存し、hexdumpで中身を見てみる。

In [13]:
np.save("a.T.copy.npy", a.T.copy())
In [14]:
!hexdump -C a.T.copy.npy
00000000  93 4e 55 4d 50 59 01 00  76 00 7b 27 64 65 73 63  |.NUMPY..v.{'desc|
00000010  72 27 3a 20 27 3c 69 38  27 2c 20 27 66 6f 72 74  |r': '<i8', 'fort|
00000020  72 61 6e 5f 6f 72 64 65  72 27 3a 20 46 61 6c 73  |ran_order': Fals|
00000030  65 2c 20 27 73 68 61 70  65 27 3a 20 28 32 2c 20  |e, 'shape': (2, |
00000040  32 29 2c 20 7d 20 20 20  20 20 20 20 20 20 20 20  |2), }           |
00000050  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |                |
*
00000070  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 0a  |               .|
00000080  01 00 00 00 00 00 00 00  03 00 00 00 00 00 00 00  |................|
00000090  02 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|
000000a0

C_CONTIGUOUSがTrue、F_CONTIGUOUSがFalseになっていたことからも分かるように、ヘッダーのfortran_orderがFalseになっている。

当然、再度np.loadで読み込んだ場合でもこの.flagsは保たれている。

In [15]:
np.load("a.T.copy.npy").flags
Out[15]:
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False

元のndarrayと、転置した上で.copy()を呼んだ後のndarrayをnp.saveしたバイナリを比較してみる。

In [16]:
%%bash
diff <(hexdump -Cv a.npy) <(hexdump -Cv a.T.copy.npy)
9,10c9,10
< 00000080  01 00 00 00 00 00 00 00  02 00 00 00 00 00 00 00  |................|
< 00000090  03 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|
---
> 00000080  01 00 00 00 00 00 00 00  03 00 00 00 00 00 00 00  |................|
> 00000090  02 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|

両者ともにC形式配列となっているため、純粋にデータ部の並びのみが異なる。

同様に転置を取った場合と、その上で.copy()を呼んだ場合のバイナリを比較してみる。

In [17]:
%%bash
diff <(hexdump -Cv a.T.npy) <(hexdump -Cv a.T.copy.npy)
3,5c3,5
< 00000020  72 61 6e 5f 6f 72 64 65  72 27 3a 20 54 72 75 65  |ran_order': True|
< 00000030  2c 20 27 73 68 61 70 65  27 3a 20 28 32 2c 20 32  |, 'shape': (2, 2|
< 00000040  29 2c 20 7d 20 20 20 20  20 20 20 20 20 20 20 20  |), }            |
---
> 00000020  72 61 6e 5f 6f 72 64 65  72 27 3a 20 46 61 6c 73  |ran_order': Fals|
> 00000030  65 2c 20 27 73 68 61 70  65 27 3a 20 28 32 2c 20  |e, 'shape': (2, |
> 00000040  32 29 2c 20 7d 20 20 20  20 20 20 20 20 20 20 20  |2), }           |
9,10c9,10
< 00000080  01 00 00 00 00 00 00 00  02 00 00 00 00 00 00 00  |................|
< 00000090  03 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|
---
> 00000080  01 00 00 00 00 00 00 00  03 00 00 00 00 00 00 00  |................|
> 00000090  02 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|

この場合、ヘッダーもデータ部も異なっていることが分かる。

なぜこの記事を書いたか

とある.npyファイルのデータ部分を直接読み込むCプログラムを触っていた際に、.Tで転置した.npyを読み込んでも読み込んだデータ部分が全く変化していないということが発生したためである。 当該プログラムではヘッダーのfortran_orderを完全に無視していたため、.Tしただけのndarrayを保存した.npyではデータ部分が転置されておらず、意図しない動作となってしまっていた。これを解決するためには前述のように.copy()を呼んだ上で保存した.npyファイルを読み込む必要があった。

コメント

Comments powered by Disqus