Kitsune Gadget

気になったことをまとめるそんな場所です

バイナリデータの処理方法はどちらが速いのか

ドワンゴがVRの3Dモデルアバター向けにVRMフォーマットを展開したということで、ページをちょっと見ていたのだけど その中でpythonによるglbのパースの例が書かれていて、前にJPEGGPS情報を取得しようとしてバイナリレベルからやったのを思い出した。 これを例に、前にやったやつは少しリファクタリングしてみた。

そこで少し気になったのは、バイト列バイナリデータを処理する方法は struct ライブラリを使う方法と numpy ライブラリを使う方法がある。 これどっちが計算速いんだ…?

というわけで比較してみる

バッファーは適当なwavデータを使った。 まず先頭4バイトの列で比較してみる。先頭の4バイトはwavのヘッダーならRIFFという文字が取得できる。今回は取得するデータの中身は関係ないけど。 ipythonで以下を実行してからその状態でdataを使う。

import struct
import numpy as np

with open("Track_01.wav", "rb") as f:
    data = f.read(4)

struct.unpack(fmt, bufffer)

fmtはフォーマットを指定する。4バイトなので'I'(unsigned int)。bufferは入力するバイト列バイナリデータ。 unpackメソッドはbuffer長さとfmtで指定した長さが同じでないとエラーが起きる。

numpy.frombuffer(buffer, dtype, offset, count)

こちらはnumpyのバッファー処理メソッド。bufferに入力するバッファーを入れる。dtypeは'I'もしくは'uint32'(4Byte=32bit)。 offsetとcountは今回は使わない。

これらのふたつで、計算速度を算出してみる。

結果(4バイト)

In [1]: run loadwav.py

In [2]: %timeit data_st = struct.unpack('I',data)
232 ns ± 7.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [3]: %timeit data_np = np.frombuffer(data, dtype='I')
1.41 µs ± 45.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

unpackのほうは232ナノ秒でfrombufferは1.41マイクロ秒 つまりunpackの方が5、6倍くらい速い

結果(64バイト)

一気に取得しちゃえー!!

In [9]: %timeit data_st = struct.unpack('16I',data)
454 ns ± 9.92 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [10]: %timeit data_np = np.frombuffer(data, dtype='16I')
31.1 µs ± 1.47 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

結果(128バイト)

In [14]: %timeit data_st = struct.unpack('32I',data)
622 ns ± 26.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [15]: %timeit data_np = np.frombuffer(data, dtype='32I')
30.8 µs ± 856 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

結果(1024バイト)

In [17]: %timeit data_st = struct.unpack('256I',data)
2.21 µs ± 81.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [18]: %timeit data_np = np.frombuffer(data, dtype='256I')
31.2 µs ± 1.6 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

サイズが大きくなるに連れて、unpackのほうは徐々に増えてそうだが、frombufferのほうはある程度で速度は落ち着いている。 (そもそもこんなにまとめて取得することは無いと思うけど…)

じゃあ、連続取得する場合はどうだろうか。

データ全てを2バイトずつ読み取って配列に入れる

with open("Track_01.wav", "rb") as f:
    data2 = f.read()

struct.iter_unpack(fmt, buffer)

bufferのデータをfmtで指定した長さごとに取得して、イテレータとして順に返す関数。 2バイトなので'H'(unsigned short)で指定。 最後に拾ったbufferの長さが指定フォーマットより短くなるとやはりエラーが起こるので注意。

numpy.frombuffer(buffer, dtype, offset, count)

こちら側は前と同じ。特に指定しなくてもdtypeの長さで順に配列にしてくれる。とっても便利。 dtype='H' もしくは 'uint16'

結果(2バイト:データ全体)

In [2]: %timeit data_st = list(struct.iter_unpack('H', data2))
3.07 s ± 64.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [3]: %timeit data_np = np.frombuffer(data2, dtype="uint16")
2.2 µs ± 38.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

iter_unpackは3秒程度掛かるのに対してfrombufferは2マイクロ秒程度で配列に収めている。 frombuffer恐るべし。 しかもiter_unpackの方はメモリの使用量もやばかった気がする…

わかったこと

  • ある部分のデータを単一的に取得する場合はstruct.unpackが速い

  • 画像や音声などで連続的データを順に配列格納する場合はnumpy.frombufferが速い

そもそもループして取得する場合にpythonのforやイテレーターが遅いためだと思うが、 配列処理はnumpyさすがですね。

参考

7.1. struct — バイト列をパックされたバイナリデータとして解釈する — Python 3.6.5 ドキュメント

numpy.frombuffer — NumPy v1.12 Manual

bufferをndarrayに高速変換するnumpy.frombuffer関数の使い方 - DeepAge