
2026/01/15 16:49
**WAT パーサーの性能向上**
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
改善された概要
wasm-language-tools ライブラリは、WAT パーサの完全な書き直しにより v0.5 で大幅な性能向上を実現しました。複雑な WebAssembly Text Format スニペットでベンチマークを行った結果、旧パーサは約 59 µs を要したのに対し、新実装では約 13 µs で完了します。これは約 350 % の改善であり、統計的に有意な差であって偶然によるものではありません。
主な技術的変更点
-
レキサの最適化
- よく使われる括弧やキーワードは
に格納された既知の緑色トークン/ノードとして事前にクローンされ、再度割り当てを行わないようにします。LazyLock - キーワード認識はバイトレベルでプレフィックスマッチング(
)を使用し、その後次の文字が識別子文字でないか確認することで「function」のような偽陽性を防止します。self.input.as_bytes().starts_with(keyword.as_bytes()) - 文字列とコメント以外のトークンは
を用いて UTF‑8 境界チェックをスキップして生成されます。get_unchecked
- よく使われる括弧やキーワードは
-
カスタムトークン型 – レキサは直接
を作成する代わりに、軽量なrowan::GreenToken
(Token<'s>
,kind: SyntaxKind
)を発行します。text: &'s str -
パーシング戦略 – パース中に子トークン/ノードを保持するための単一共有
を使用し、必要に応じてそれを消費してVec
を生成します。スタック型の開始インデックス(rowan::GreenNode
)で範囲を追跡します。usize
これにより、多くの一時的ベクタや不要な
呼び出しが発生していた以前のunwrap
アプローチを回避できます。rowan::GreenNodeBuilder
この書き直しは、従来の
winnow パーサ―コンビネータライブラリを手作りのレキサ/パーサに置き換え、多数のメモリアロケーションとチェックを排除しました。高速化されたパーサは v0.5 以降のデフォルトエンジンとして採用される予定で、wasm-language-tools をベースにしたすべてのツールがより効率的に動作します。大量の WAT を生成・操作する開発者や企業にとって、この改善はビルド時間の顕著な短縮、IDE とのインタラクションのスムーズ化、および開発・デプロイ時のリソース使用量の削減へ直結します。本文
元の WAT パーサは遅かった
wasm-language-tools v0.5 以前における WAT(WebAssembly Text Format)パーサは、性能期待を満たせず遅延していました。完全に書き直した結果、ベンチマークで 350 % の向上が確認されました。
パーサの最適化手法
-
ハンドメイドのパーサを書く
従来は
(コンビネータライブラリ)を使用していました。便利ですが、一般的にコンビネータは命令型パーサより遅くなります。自作パーサなら速度が速く、さらに最適化しやすいです。winnow -
よく使われる緑色トークンとノードをクローン
WAT は括弧やキーワードが多いため、それらを毎回生成するとコストがかさみます。
/rowan::GreenToken
は内部でGreenNode
を保持しています。よく使われるトークン・ノードを事前に作成し、Arc
に格納しておけば、必要時にクローンするだけで済みます。LazyLock -
キーワードマッチング
単語を先に字句解析した後にリストと比較するのではなく、ソース文字列の接頭辞を直接チェックします。self.input.as_bytes().starts_with(keyword.as_bytes())「
がfunction
で始まる」などの誤検知を防ぐために、次の文字が識別子文字でないことも確認します。func -
ASCII トークンには
を使用get_unchecked
文字列やコメント以外は純粋な ASCII です。
を使うと、str::get_unchecked
が行う不要な UTF‑8 境界チェックを省けます。String::from_utf8 -
軽量
型を自前で定義Token
ライザは
(完全なToken<'s>
の代わり)を発行します。これにより割当オーバーヘッドが削減されます。rowan::GreenTokenstruct Token<'s> { kind: SyntaxKind, text: &'s str, }
を実装すれば、構文木を作る際に変換が容易になります。impl From<Token<'_>> for rowan::NodeOrToken<rowan::GreenNode, rowan::GreenToken> -
ノードごとの Vec 割当を回避
構文木には多くのノードがあります。各ノードで新しい
を割り当てると頻繁な割当・解放が発生します。Vec
に触発され、単一の共有rowan::GreenNodeBuilder
を保持します。Vec- パース中は子トークン/ノードをこのベクタに push。
- ノード完了時には、記録した開始インデックスから現在長さまでをスライスとして取り出し、
のコンストラクタへ渡します。GreenNode
この手法はスタックのように機能しますが、明示的なスタック構造を排除できます。
-
結果
最終的に作成したパーサは、以前よりもシンプルかつ高速です。以下はベンチマークで使用されたコードスニペットです。
(module (func $f1 (param $p1 i32) (param $p2 i32) (result i32) (i32.add (local.get $p1) (local.get $p2)) ) (global $g1 f64 (f64.const 0)) (func $f2 (result f64) (global.get $g1) ) (type $t (func (result f64))) (func $f3 (type $t) (call $f2) ) (func (export "f32.min_positive") (result i32) (i32.reinterpret_f32 (f32.const 0x1p-149))) (func (export "f32.min_normal") (result i32) (i32.reinterpret_f32 (f32.const 0x1p-126))) (rec (type $r (sub $t (struct (field (ref $r)))))) (global (;7;) (mut f32) (f32.const -13)) (rec (type $t1 (sub (func (param i32 (ref $t3))))) (type $t2 (sub $t1 (func (param i32 (ref $t2))))) ) (global (;8;) (mut f64) (f64.const -14)) (func (export "f32.max_finite") (result i32) (i32.reinterpret_f32 (f32.const 0x1.fffffep+127))) (func (export "f32.max_subnormal") (result i32) (i32.reinterpret_f32 (f32.const 0x1.fffffcp-127))) )
最適化後のベンチマーク結果は以下の通りです。
| パーサ | 時間(µs) |
|---|---|
| old | 59.473 – 59.648 |
| new | 13.004 – 13.299 |
新しいパーサは旧バージョンを一貫して上回り、約350 % の速度向上を達成しています。