1. まず第一にMATLABは便利
まず、なぜMATLABでバイナリファイルなんか読み込みたいのかというと、
デバッグに便利だから!!
これに尽きます。他のソフトで開発して、その出力をMATLABで読み込めれば、可視化がそれはそれはもうお手軽にできます。
いろんなものを可視化してバグを見つけたり、レビュー用にグラフを作ったりがしやすくてしょうがないです。
2. そのくせバイナリ読み込みだけハードルが高いMATLABさん
とはいえバイナリファイルの読み込みだけがc++レベルにめんどくさいMATLABさん。(他のも同レベですが)
おそらくバイナリ読み込みに使える関数はfreadかmultibandreadくらいじゃないかと思いますが、大抵はfreadでなんとかなります。multibandreadの方が少し込み入った設定ができる程度なので、この記事では基本freadで説明します。
2.1 freadの基本
freadはfopenやfcloseとセットで使ってバイナリを読みます。
% doubledata.bin
% 8 3 4 1 5 9 6 7 2
fileID = fopen('doubledata.bin');
A = fread(fileID,[3 3],'double')
fclose(fileID);
% A = 8 1 6
% 3 5 7
% 4 9 2
2つ目の引数にサイズを入れて、3つ目に型を指定すると、指定した型とサイズの分だけバイナリを読み進めて、行列として出力してくれます。これは非常に便利!
大きいファイルを末尾まで読み込むときはwhile ~feofを使って
filename = 'largedata.dat'; % hypothetical file
segsize = 10000;
fid = fopen(filename);
while ~feof(fid)
currData = fread(fid, segsize, 'double');
end
fclose(fid);
とします。feofは今読み込んでいるのがファイルの末尾かどうかを返すものです。
2.2 あと一歩なんだよなぁ
「え、なに便利じゃん。」と思ったかもしれませんが、こんな落とし穴があるのです。
例えばdoubleとintが交互に並んだバイナリファイルを読みたいとしましょう。
するとfreadさんの3つ目の引数である型指定が悪さをしてきます。
この型指定、ひとつの型しか受け付けないのです。doubleと指定したらそのfreadではdoubleでしか読めないのです。
となるとdoubleとintが交互に並んだバイナリファイルはこう読み込むしか無いのです。
filename = 'largedata.dat'; % doubleとintが交互に入っている。
fid = fopen(filename);
while ~feof(fid)
d_data = fread(fid, 1, 'double');
i_data = fread(fid, 1, 'int32');
end
fclose(fid);
せっかくサイズ指定で読み込む機能があるのに、型には柔軟じゃないのです。いや、有料のMATLABさんのことなので、型指定の部分を["double" "int"]
とベクトルで指定したら、指定したサイズのセルで出力してくれるとかそう言うのがあると思ったんですよ。でも残念ながらそんな機能は無いのです。
しかし、普通なら「それで読めるならいいじゃん」と思うでしょう。それがよくないのです。
手元で試してみたところ、ループ回数が増えるほど爆裂に遅くなっていくのです。まあ当たり前ですよね。これはMATLABのループが遅いのか、freadの呼び出しが遅いのか不明ですが、なんにせよ値をひとつひとつ読み込んでいては効率が悪い!2023bあたりには直してほしい!
3. 高速読み出しを編み出した。
僕はそこでへこたれるような人材ではないので、高速に読み出す方法を考えました。
コンセプトから言うと、
まず1バイト型で全部読んでキャストする
です。MATLABにもキャストがあった。ラッキー。
こちらがそのコードです。
下記のコードでfreadとreshapeを連続して書いてますが、freadのサイズ指定を[12,inf]にすればreshapeいらなかったです。
filename = 'largedata.dat'; % doubleとintが交互に入っている。
fid = fopen(filename);
data = fread(fid,'int8=>int8'); % とりあえず全部1バイト型で読む
data = reshape(data, 12, []); % doubleは8バイト、intは4バイトなので、8+4=12行になるように変形。
% dataの1行目から8行目までがdoubleの数値を示しているので、そこを切り取ってキャスト。
d_data = data(1:8,:);
d_data = typecast(d_data(:), 'double');
% intも同じく切り取ってキャスト。
i_data = data(9:12,:);
i_data = typecast(i_data, 'int32');
fclose(fid);
こうすることでループがなくても別の型の読み込みができます。
よくわからんと言う人のために説明すると、fread(fid, 'int8=>int8')
は、int8で読み込んで出力もint8にしますって言う意味です。単に'int8'だと出力の型がdouble
に変えられてしまうので'int8=>int8'としてます。これで、1バイトずつの配列としてdata
に格納されます。正直int8じゃなくても1バイト型ならなんでもいいです。
これをreshape
で12行の行列に変換します。こうすると、1行目から8行目にはdoubleを表現するバイトが、9から12行目にはintを表現するバイトが並びます。
そして最後にtypecast
を使って、1行目から8行目をごっそりdoubleに解釈変更。9から12行目をごっそりintに解釈変更します。typecastはビットの並び順を変えずに、型の解釈だけを変える関数です。
4. 計測したら200倍近く速いぞ!
ひとつずつ値を読み込むものと、一気に読み込んでキャストするものとで速度比較しました。
元データはdoubleとintを交互に100,000回ずつ書き込んだものです。
ひとつずつ | 一気に読む |
---|---|
9.235848s | 0.053059s |
なんと約174倍も高速に読めました!
5. ちなみにPythonくんの場合
Pythonは一般にMATLABよりは動作が速いことで知られています。
Pythonで同じ読み方をさせた場合どれくらい速いのか試してみました。
Pythonでもnp_d_dataとnp_i_dataのreshapeは要らないようです。余計なことをしました。
import numpy as np
data_byte = 12
with open('testdata.bin', 'rb') as f:
data = f.read(-1)
np_data = np.frombuffer(data, dtype=np.int8)
np_data = np_data.reshape(-1,12)
np_d_data = np_data[:,0:8]
np_i_data = np_data[:,8:12]
np_d_data = np_d_data.reshape(1,-1)
np_i_data = np_i_data.reshape(1,-1)
# np.arrayのまま変換できる。
d_data = np.frombuffer(np_d_data, dtype='double')
i_data = np.frombuffer(np_i_data, dtype='int32')
結果は0.025s!!!
さすがです。
いろいろと試した結果、pythonではnp.arrayを使ったこのコードが一番速かったです。バイナリを読むといったらstructのunpackを使うとかありますが、numpyのfrombufferが意外にも優秀でした。
6. PythonコードをMATLABから呼び出す
さらに変態技を試してみました。
Pythonでファイル読み出しのコードを書いて、それをMATLABから呼び出す方法です。これは結果だけ言いますが、読み出すだけならPython単体とほぼタイムは変わらず、MATLAB用の型に変換するのに少々時間がかかって、結局0.05sほどになりました。
PythonとMATLABの並行運用しないといけない場合はこの方法を使うのがいいでしょうね。
誰かの参考になれば幸いです。