Rust+WasmでFIXログパーサーを作る
Wasm FIX Parser
Rustの練習として金融関係で使われるFIXプロトコルのパーサーWebアプリをWasmで作った。以前作ったJSバージョンをRustへ移植した。
レポジトリはこちら。
FIXプロトコルとは
FIXプロトコルは証券やFXの売買で30年くらい使われている古いプロトコルで、要はキーバリューペアを一行に並べて送信するだけのもの。
例えばこのメッセージを見てみる。
35=D|49=ABANK|56=EXCHANGE|38=1000000|44=1|54=2|55=JCOM
キー35
はメッセージタイプ、その値D
は注文発注などというマッピングの取り決め(プロトロル)が双方で共有されているため、以下のようにパースされる。
MsgType (35) : ORDER_SINGLE (D)
SenderCompID (49) : ABANK
TargetCompID (56) : EXCHANGE
OrderQty (38) : 1000000
Price (44) : 1
Side (54) : SELL (2)
Symbol (55) : JCOM
つまり、ABANKがEXCHANGEに対しJCOM株を1円で100万株の売り注文を出したと解釈される。
タグや値のマッピングは、バージョンや金融機関ごとにXMLファイルに定義するのが慣例となっている。
金融関連に長くいる人は主要なタグを暗記していて頭の中で自動的に44が価格に変換されたりするようだが、普通の人はパーサーアプリで人の読めるフォーマットにする。
ロジック
ロジックは以下のように単純明快。
- キーバリューの定義を含むXMLファイルを読み取り、ハッシュマップに保存する
- FIXメッセージをキーバリュー列のトークンに分解する
- 分解されたトークンを1で作ったハッシュマップを使い人の読める文字列に変換
- 全メッセージを処理し、ウェブサイトに表示可能な形にフォーマット
典型的なレキサー、パーサー、フォーマッターの流れ。
JavaScriptとの(適当)パフォーマンス比較
結論
wasmのほうが2倍速かった。
概説
数年前に作った同じパーサーアプリのJSバージョンとパフォーマンスをざっくりと比較してみた。
- 方法: 1000行を読み込ませChromeのプロファイラでScript部分(Renderingではない部分)を計測する
- 結果: Wasmバージョンは約50ms, JSバージョンは約100ms
JSバージョンでは、XMLファイルを事前にオブジェクトリテラルに変換したものを使っているため厳密には同じではない。
JSからRustへの移植で得た知見
小さいデータは積極的にclone/copyする
RustはCからの移植が重視されているためか、不要なclone/copyはアンチパターンとされている。
個人的には、以下をすべて満たす場合はコードの生産性/見通しのためあえてコピーしてよいと思う。
- 100バイト以下のデータ
- コピーの発生回数は高々1秒間に100回
- 広範に利用されることを想定したライブラリではない
- 特殊なパフォーマンス要件がない (組み込み系, 高頻度取引など)
コピーを防ぐために構造体にライフタイム注釈をつけると、implの表記も煩雑になるし、構造体を使う側もどのインプットを生かさないといけないのか気をつかう。(自分がライフタイムを使いこなせていないのが一番の理由ではあるが)
ボトルネックになっていることがわかってきてから、Rcをつかったりライフタイム注釈をつけても遅くないと感じた。
リソースファイル読み込みは include_str
/include_bytes
マクロを使う
Java/C#でいうリソースファイルの取り扱いがRustでは簡単安全にできることに驚いた。
XMLファイル等のコードではないファイルをバイナリに含めるには、Javaではresourcesディレクトリ, C#ではPropertiesディレクトリにファイルを配置しておく。Java/C#では、ファイルを置き忘れて実行時エラーをくらったり、単体テスト用のリソースファイルを更新し忘れてテストが失敗したりと何かと使いづらい。
JSではそもそもリソースファイル的な機能がないため、Webサーバーからfetchしてくるかwebpackのローダーを使う。fetchは遅いし、ローダーはwebpackアップグレードや移行の障害になりやすい。
Rustではinclude_str
/include_bytes
マクロが用意されており、XMLファイルなどをコンパイル時にconstに入れられる。
const FIX40_XML: &[u8] = include_bytes!("xml/FIX40.xml");
指定されたファイルが存在しないと、実行時エラーではなくコンパイルエラーを出してくれる。
イテレーターの長いワンライナーは避けてforループを使う
JSでは、(良くも悪くも)以下のようなワンライナーを書くことがある。
const result = arr.map(x => ...)
.filter(x => ...)
.flatMap(x => ...)
.reduce(...);
Rustで同じことをやろうと試行錯誤したが、長くなりそうなときは素直にfor
ループで可変なVec
にpush
すべきという結論に達した。理由は以下。
-
Result
の省略記法?
がクロージャ内で使えない(使いづらい) -
&
と*
だらけになり型がわかりにくくなる。iter().filter
だと**
が必要になったりする。(公式リファレンスにもpossibly confusingと書いてある) -
map
内で&mut self
を使うメソッドを呼ぶと副作用が発生してしまう。対応する副作用のないメソッドがないことが多い
参考: イテレーターでResultの扱い方についてはこの記事が詳しい。
Author And Source
この問題について(Rust+WasmでFIXログパーサーを作る), 我々は、より多くの情報をここで見つけました https://qiita.com/yosqueoy/items/d312ada70da77e6f3611著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .