Red Data Tools のおそらく本丸とも言える最重要プロジェクトが Red Arrow です。 Apache Arrow というミドルウェアのRubyバインディングとして開発がスタートし、今では本体に取り込まれています。 とはいえ、このミドルウェアはビッグデータを効率よく処理するためのもので、あまり機械学習とは関係がないようです。 バリバリの機械学習エンジニアに聞いてみたところ、名前すら知らないという有様でした。
Apache Arrowは、大量のデータをメモリ上で効率よく処理するためのミドルウェアです。 いや、正確に言うとミドルウェアではなくライブラリと言うか、それともフォーマットと言うか、少し掴みどころがない存在です。 特定のプログラミング言語とは独立した、大量データ処理に向いた標準的なメモリフォーマットを策定しています。 Arrow自身は、公式サイトでインメモリデータのためのプラットフォームと名乗っています。
Arrowは列指向データベースをメモリ上に持ち、不必要なI/Oを避けることで高速なデータアクセスを実現しています。 また、様々なデータ処理ソフトウェアの内部メモリ表現のデファクトスタンダードを目指していて、 既にいくつもの著名なデータ処理ソフトウェアはArrowのメモリフォーマットをサポートしているようです。 そのため、それらのソフトウェア同士が連携する際に、JSONなどのシリアライズが必要なデータではなく、 シリアライズが不要なデータを直接高速に読み込むことが可能なようです。
と、公式サイトに書かれていることを要約してみたところで全く意味がわかりません。 当然他のプロセスのメモリにアクセスなんてできないはずなので、ファイルを介してしまったらI/Oもへったくれもなさそうな気がします。 そのフォーマットにしても、たとえばRubyistのまわりには大変有名な msgpack と何が違うんでしょうか。 実際に試してみましょう。
Red Arrowのサンプル の read-file.rb
でArrowファイルを作り、write-file.rb
をPythonに移植して読み込んでみます。
まず、Arrowファイルを書き込むコード を見ていきましょう。
最初にスキーマの定義をしています。
Rubyっぽくない大量のインスタンス生成があり冗長なコードです。
次にファイルをオープンし、データを書き込んでいます。
ここで不思議なのは、 [Int, String, Boolean]
というスキーマに対し、
[
[1, 'a', true],
[2, 'b', false]
]
のような1行ごとのデータではなく、
[
[1, 2],
['a', 'b'],
[true, false]
]
のように列ごとにデータを渡しているところです。 これはストリーム版でも同じで、なぜこのようなデータ形式なのかよくわかりません。
次にArrowファイルを読むコード を見てみます。
record_batch.map
で values
を作っているので、 record_batch
は行データであることがわかります。
列ごとにデータを渡した理由は、読みこむ際に特定列にアクセスしやすくするためかと思いましたが、どうやらそうでもないようです。
一方、 Pythonに移植したコード の場合、 record_batch.column(j)
で列のデータが取れるので全く仕様が違うようです。
こうなったら書く方もやはりPython版を作ってみないといけません。 Ruby版と全く同じになるようにコードを書いていきます。 見た感じ圧倒的にPythonのほうが自然なインタフェイスのような感じがします。 (おそらくJava版を)そのままマッピングしたようなクラス構成をやめ、RubyはRubyらしく DSLを使って定義できたほうが便利でしょう。
データの与え方は、やはりPythonも列ごとにまとめるようです。 Ruby版はこれをそのまま持ってきたのでしょうか。 実行してみると、Python版もRuby版も、出力したファイルは完全に一致しました 当然、互いの言語で正常に読み込みできます。
まず、RedArrowとpyarrowのパフォーマンス測定をします。
前述の書き込みと読み込みを何度か繰り返して time
コマンドで測定します
python | ruby | |
---|---|---|
write * 100 | 0.219 | 0.161 |
write * 10000 | 0.931 | 2.119 |
read * 10 | 0.231 | 0.249 |
read * 1000 | 0.333 | 1.385 |
回数が少ない場合、むしろRubyのほうがパフォーマンスが良いこともありますが、
回数が増える(あるいはデータ量が多くなる)と圧倒的に低速になります。
time
の出力によるとRubyはCPUが100%を超えることがなく、どうやらRedArrowはシングルコアしか使えてないようです。
特にreadは極端に性能悪化します。
each
と for
の世界観の違いもあり、実際のアプリケーションでも同じように極端な性能差になるかどうかはわかりません。
次に、Arrow以外のフォーマットとのパフォーマンス比較したいと思います。
ここでは、実際のログファイルのようなデータ構造、[string uint32, float32, string, string, uint16, string]
なスキーマのデータを読み込んでみます。
何パターンかのレコード数で、JSON MessagePack Arrow の各フォーマットでファイル出力・読み込みを実行し、かかった時間を計測します。
もちろん使う言語はRubyです。
まずは、書き込み のパフォーマンスを測定します。
顕著に違いがあったのは user
時間なので、user
時間を掲載します。
system
時間はおそらくファイル入出力の時間で、大体どのライブラリも同じくらいの時間がかかっていました。
レコード数 | json | msgpack | arrow |
---|---|---|---|
10 | 0.000000 | 0.004175 | 0.011235 |
1000 | 0.094390 | 0.007750 | 0.011660 |
100000 | 11.516991 | 4.976050 | 0.079441 |
結果は一目瞭然となりました。 レコード数が増えれば増えるほど、他のフォーマットでは遅くなりますが、Arrowはそれほど遅くなりません。 おそらく、Arrowは オーバーヘッドが多少大きめ で0.011230秒くらいあり、100倍、1万倍の時の伸びはおおよそ定数倍に伸びている感じがします。 一方MessagePackは明らかに1000レコードから10万レコードの中間に 何かがあり 、実行時間は700倍に伸びています。 100万レコードになるとMessagePackが最も低パフォーマンスになりました。
なお、ファイルサイズは常に、 JSON > Arrow > MessagePack でした。
次に、読み込み のパフォーマンスを測定します。
今回は、ファイルキャッシュの影響か user
と system
で負荷が行ったり来たりするので total
を掲載しています。
レコード数 | json | msgpack | arrow |
---|---|---|---|
10 | 0.001694 | 0.001292 | 0.005406 |
1000 | 0.104062 | 0.046994 | 0.003519 |
100000 | 10.909454 | 4.408111 | 0.003798 |
Arrowが圧倒的に速いことがわかります。 なぜかArrowはレコード数10のときに一番パフォーマンスが悪化しています。 不思議なのですが、何度やり直してもこのような結果になりました。 それ以降は1000でも10万でもほぼ誤差のような数値でしかないようです。
ただ、この測定方法には疑問があります。 JSONやMessagePackはファイルを全部読み込んでパースしているのですが、Arrowはあまりに速すぎるのでロードを遅延させているのではと疑っています。 とはいえ、JSONなんかは全体を読み込まないと当然パースできないので、そういうことができるだけでも大いに有用だと言えるでしょう。