CRDTを5000倍高速化した最適化の道のり (2021)
(josephg.com)- 協業編集でCRDTが遅いという評価は、しばしばアルゴリズム自体と実装方式を混ぜて判断した結果であり、データ構造とメモリ配置だけでも性能は大きく変わりうる
- Automerge v1.0.0-preview2 は 260,000 件の編集トレース処理に 291秒 と 880MB RAM を使い、同じ処理を Diamond types native は 56ms と 1.1MB RAM で処理した
- Yjs はツリーの代わりにフラットなリスト、位置キャッシュ、双方向連結リスト、span の保存を活用し、同じトレースを 0.97秒 と 3.3MB RAM まで削減した
- Diamond types は Rust で range tree/B-tree ベースの構造を使い、位置検索・挿入・削除を
log(n)時間で処理し、WebAssembly でも Node.js ベースで 193ms を記録した - このベンチマークは単一ユーザーのローカル編集再生と RAM 使用量だけを見たものなので、実際の選定では保存・読み込み時間、ネットワーク/ディスクサイズ、binary encoding、プロトコル、presence、editor binding まであわせて見る必要がある
アルゴリズムと実装性能は別物
- ある学術比較では、Google Docs のようなリアルタイム協業編集を複数の CRDT と OT アルゴリズムで実装してベンチマークし、一部のアルゴリズムは単純な paste 処理に 3秒以上かかった
- 遅かった方式は ShareJS や Google Wave で使われていたアルゴリズムだったが、その実装では 1000 文字の paste を 1000 個の個別 operation に分割して処理していた
- この事例は、同時編集の動作と実装方式を分けて見る必要があることを示している
- 動作とは、同時編集が同じ領域に入ってきたときに、どのような順序と規則でマージされるかを意味する
- 実装には、プログラミング言語、データ構造、最適化の水準が含まれる
- 同じ text OT transform 関数でも、JavaScript では毎秒約 100,000 回、C では毎秒 20M 回実行され、約 200倍の差が出る
- ある遅い実装ひとつが、そのシステムのすべての実装が遅い証拠になるわけではなく、より高速な実装は可能である
CRDT と Automerge の基本モデル
- CRDT は複数のユーザーが同じデータを同時に編集できるようにし、ローカルで遅延なく作業して後で同期し、eventual consistency に到達できる
- Automerge は Martin Kleppmann が作った協業編集ライブラリで、RGA アルゴリズムに基づいている
- Automerge や Yjs のような CRDT は共有ドキュメントを文字のリストとして扱い、各文字に一意な ID を付与する
- 空の文書に
abcを入力すると、(seph, 0)、(seph, 1)、(seph, 2)のような ID が付く - 新しい文字には「どの項目の後ろに挿入されるか」もあわせて記録する
- 空の文書に
- Automerge/RGA は各項目に sequence number を追加する
- 新しい項目は、それまでに見た最大の sequence number より 1 大きい値を受け取る
- 子項目が複数ある場合は sequence number の大きい順に並べ、同じなら agent ID で並べる
- Automerge/RGA の動作は 3 段階で捉えられる
- 各項目を parent に接続してツリーを構成する
- 子が複数ある項目は sequence number と ID で整列する
- ツリーを depth-first traversal でフラット化し、最終的なリストまたはテキスト文書を作る
Automerge ベンチマークとボトルネック
- ベンチマークは automerge-perf の編集トレースを使用した
- Martin Kleppmann が学術論文をタイピングした内容を文字単位で記録した trace である
- trace には 260,000 件の編集があり、最終文書サイズは約 100,000 文字である
- 同時編集は含まれていない
- テストではローカルに trace を適用する時間だけを測定した
- 環境は Ryzen 5800x ワークステーション、Nodejs v16.1、Rust 1.52 である
- Automerge v1.0.0-preview2 はこの trace の処理に 291秒 かかり、完了時点の RAM は 880MB だった
- キー入力 1 回あたり約 10KB RAM を使った計算になる
- ピーク RAM は 2.6GB だった
- 遅い spike では単一編集の処理に 1.8秒かかった
- JavaScript 文字列に直接 splice する baseline は同じ編集を 0.61秒 と 0.1MB RAM で処理したが、これは協業編集に必要な情報をすべて捨てた比較用 baseline である
- Automerge が遅いのにはいくつかの実装上の理由がある
- 文書が大きくなるほど、ツリーベースのデータ構造が肥大化して遅くなる
- Immutablejs を多用しており、V8 optimizer と GC が最適化しにくい
- 挿入された各文字を個別の項目として扱うため、paste も多数の項目として処理される
- Automerge チームは Rust 実装である automerge-rs を WASM で使う代替実装を進めていた
- 当時の master branch ベースでは、このテストの平均性能は速くならなかったものの、メモリ使用量は半分に減り、性能変動は緩やかになった
Yjs 式フラットリスト実装
- Yjs は Kevin Jahns が作ったオープンソース CRDT 実装で、ツリーの代わりにすべての項目を単一のフラットリストに保存する
- フラットリストへのアクセスは、新しい項目を parent の直後からスキャンして挿入位置を探す方式であり、list CRDT をリストとして実装する形である
- 実験用の reference-crdts は Yjs の YATA と Automerge の RGA を同じ方式で実装している
- このアプローチの利点は 3 つある
- 不均衡なツリーの代わりに flat array を使うため、より小さく高速である
- コードが単純である
- Yjs、Automerge、Sync9 など複数の list CRDT に適用できる
- 理論上は、同じ位置への同時挿入が多いと遅くなる可能性があるが、実際の編集では大半が parent の直後に挿入される
- reference-crdts 実装は Automerge より約 10 倍高速で、RAM は約 30 倍少なく使用した
| テスト | 処理時間 | RAM 使用量 |
|---|---|---|
| automerge v1.0.0-preview2 | 291s | 880MB |
| reference-crdts Automerge/Yjs | 31s | 28MB |
| Plain string edits in JS | 0.61s | 0.1MB |
スキャンと挿入コストの削減
- フラットな array 実装には2つのボトルネックが残っている
- 挿入位置を見つけるために文書をスキャンする必要がある
doc.content.splice(destIdx, 0, newItem)で array の途中に挿入すると、後ろの項目を移動しなければならない
- 削除された項目は他の挿入から参照されうるため array から取り除けず、
isDeletedのような印を残す必要がある- 現在の文書が100,000文字でも、過去の項目まで含めると 150,000 個の array item が存在しうる
- 文書位置 50,000 に挿入するには、削除項目を飛ばしながらおよそ 75,000 個の item をスキャンすることがある
- このような構造では、項目が
n個ある文書に挿入するたびにおよそnステップが必要で、n個の文字を挿入すると O(n²) になる - Yjs は人が文書を編集するやり方に合わせて、最後の編集位置の
(index, position)ペアをキャッシュする- 次の編集は前の編集位置の近くで起きる可能性が高いため、前後に短くスキャンする
- 複数の利用者が別の位置を編集する場合に備えて、キャッシュした位置の集合を保存する
- Yjs は array の代わりに 双方向連結リスト を使い、位置を見つけた後は定数時間で挿入する
- さらに、人が文字を連続して入力する点を利用し、
helloを5つの文字項目ではなく1つの span として保存する- ID と parent が連続してつながっている場合にのみ collapse できる
- このデータセットでは array entry 数が 180,000 個から 12,000 個へ減り、14倍削減 となった
| テスト | 処理時間 | RAM 使用量 |
|---|---|---|
| automerge v1.0.0-preview2 | 291s | 880MB |
| reference-crdts Automerge/Yjs | 31s | 28MB |
| Yjs v13.5.5 | 0.97s | 3.3MB |
| Plain string edits in JS | 0.61s | 0.1MB |
Rust と Diamond types の range tree アプローチ
- JavaScript オブジェクトは content、deletion flag、ID、seq、parent などがポインタで散在した構造になりやすく、メモリ断片化とキャッシュミスのコストが大きい
- Rust はメモリ配置を直接制御でき、WebAssembly を通じて Web でも使える
- Diamond types は Rust で書かれた CRDT 実装で、Yjs とほぼ同じだが、内部的には連結リストではなく range tree を使う
- 内部の range tree は少し修正した B-tree である
- 一般的な
BTreeMapのように key を保存する代わりに、内部ノードが子に含まれる文字数の合計を保存する - 文書の任意位置の参照、挿入、削除を
log(n)時間で処理する
- 一般的な
- 260,000 件の編集 trace はこの tree では leaf node 3段程度に収まり、どの item もおおむね3回のメモリ read で見つけられる
- remote edit merge のために ID で B-tree を引く小さな index もあるが、その codepath はこのベンチマークには含まれていない
- leaf node は 32 entry ブロックを固定長 array で密に格納する
- 32 という bucket size は複数サイズでベンチマークした結果うまく機能したが、なぜ最適なのかは分からないとしている
- Diamond を diamond-js として WASM コンパイルし、Node.js から呼び出すと、同じ trace を 193ms で処理する
- Yjs より約5倍速い
- JavaScript 文字列の baseline より約3倍速い
- ネイティブ Rust 実行は benchmark で 56ms を記録した
- Automerge より 5000 倍以上速い
- 毎秒 4.6M operation を処理する
- 全 260,000 件の編集処理中、
malloc呼び出しは 1394 回だった
| テスト | 処理時間 | RAM 使用量 |
|---|---|---|
| automerge v1.0.0-preview2 | 291s | 880MB |
| reference-crdts Automerge/Yjs | 31s | 28MB |
| Yjs v13.5.5 | 0.97s | 3.3MB |
| Plain string edits in JS | 0.61s | 0.1MB |
| Diamond WASM via Node.js | 0.19s | 不明 |
| Diamond native | 0.056s | 1.1MB |
Ropey 分離と tradeoff
- Diamond の実装は、文書テキストの内容を CRDT item リストに直接入れず、別のデータ構造に保存する
- テキスト内容には Rust ライブラリ Ropey を使い、Ropey もテキスト管理のために B-tree を実装している
- この方式には 工学的 tradeoff がある
- Ropey はテキスト特化の byte packing を行うため、RAM 使用量を減らせる
- 挿入時に2つのデータ構造を更新する必要があるため、速度は2倍以上遅くなり、WASM bundle も 60KB から 120KB に大きくなる
- VS Code のような editor と接続するなら、editor 側が文書のコピーを保持するため、CRDT 構造内に文書内容を保存しなくてもよい場合がある
- Ropey だけで trace を処理すると 29ms かかる
- Diamond native で文書 content の更新を無効にすると、23ms と 0.96MB RAM を記録した
- Automerge より約 14,000倍速い
- 毎秒 11M operation を処理する
- この結果は有用性よりも、CRDT metadata 処理の限界を見るための実験に近い
| テスト | 処理時間 | RAM 使用量 | データ構造 |
|---|---|---|---|
| automerge v1.0.0-preview2 | 291s | 880MB | naive tree |
| reference-crdts Automerge/Yjs | 31s | 28MB | array |
| Yjs v13.5.5 | 0.97s | 3.3MB | linked list |
| Plain string edits in JS | 0.61s | 0.1MB | なし |
| Diamond WASM via Node.js | 0.20s | 不明 | B-tree |
| Diamond native | 0.056s | 1.1MB | B-tree |
| Ropey Rust baseline | 0.029s | 0.2MB | なし |
| Diamond native, no doc content | 0.023s | 0.96MB | B-tree |
実際のライブラリ選定基準
- 文書ベースのコラボレーションアプリを今作るなら、Yjs を使うほうが有利である
- Yjs は性能、低メモリ使用量、サポートのエコシステムに優れている
- Kevin Jahns は Yjs 統合支援を有償で提供していることもある
- Automerge チームも性能を 2021 年の最優先課題としており、複数の手法で Automerge を高速化する計画があった
- Diamond は非常に高速だが、Yjs や Automerge と同水準の機能 parity には、まだ多くの作業が残っている
- CRDT ライブラリには operation 速度以外にも、binary encoding、network protocol、non-list データ構造、presence、editor binding などが必要である
- データベース semantics が必要なら、CRDT 上でよくできた実装は知られておらず、OT ベースの ShareDB を使える
- Redwood は P2P editing をサポートし、full CRDT support を計画しているプロジェクトである
測定方法の制約
- このベンチマークはローカル編集トレースの再生時間とRAM使用量のみを測定している
- ローカルユーザー入力は十分に高速であればよく、CRDTが単一のlocal editを約1ms以下で処理できれば、それ以上の速度はそれほど重要ではない可能性がある
- Automergeも、不運なGC pauseを除けばおおむねこの水準を満たしている
- 実際には、より重要な指標が別にある
- ドキュメントがディスクやネットワーク上で占めるbyte数
- 保存と読み込みにかかる時間
- 保存済みドキュメントをデータベース内で更新する時間
- 使用したトレースには単一ユーザーの編集しか含まれておらず、同時編集が多いpathological caseが残っている可能性がある
- 現在、YjsやAutomergeでデータベース内の単一オブジェクトを更新するには、一般にドキュメント全体をRAMに読み込み、変更し、ドキュメント全体を再保存する必要があるため、遅くなる可能性がある
- Kevinは、Yjs providerを適切に調整すれば、妥当な方法で実装できると述べている
- list CRDTは、削除済み項目のtombstoneのため基本的に増え続け、pruningは別のアプローチになる
- 例として、YjsのGCアルゴリズムとAntimatterが挙げられている
- pruningは、記事で扱ったデータ構造の最適化とは直交する問題である
比較は完全に統制された実験ではない
- 各最適化段階では複数の変数が同時に変更されており、速度向上の正確な原因は切り分けられていない
- Automergeからreference-crdtsへ移る際に変わった要素はいくつもある
- コアのデータ構造がツリーからリストに変わった
- Immutablejsが削除された
- Automerge frontend/backend protocolと複数のUint8Array構造がなくなった
- JavaScriptのスタイルが関数型からimperativeに変わった
- reference-crdtsからYjsへ、YjsからDiamondへ移る際も、変化は単一の原因として分離されていない
- automerge-rsがこのテストでAutomergeより速くなかった点は、Diamondの性能がRustだけの効果ではないことを示す根拠にはなるが、正確な寄与度はわからない
- RGAとYATAを同じ実装方式で比較することも、「同時マージ時の挙動が実質的に似ており、挙動を変えても実装性能は維持される」という前提に依拠している
- reference CRDT implementationでは、YjsとAutomergeの挙動はほぼ同一のcodepathと同一の性能を示している
- conflict-heavyなトレースでは性能差が出る可能性はあるが、実際には非常にまれなケースだと見なしている
- Yjsは各itemがいつ削除されたかは保存せず、削除済みかどうかだけを保存する
- Diamondで削除時点を保存すると、メモリ使用量は1.12MBから2.34MBに増え、約5%遅くなる
- この記事のすべてのDiamond benchmarkは、Yjs方式に合わせたyjs-style branchを使用している
ベンチマークコードと再現用資料
- JS文字列baseline、Yjs、Automerge、reference-crdtsのテストコードはGitHub gistにある
- ほとんどのテストには、josephg/crdt-benchmarksの
automerge-paper.json.gzが必要 - reference-crdts benchmarkは、該当バージョンのjosephg/reference-crdtsに依存している
- Diamond benchmarkは、該当バージョンのjosephg/diamond-typesで実行されている
- 実行コマンドは
RUSTFLAGS='-C target-cpu=native' cargo criterion yjs - memory statisticsは
cargo run --release --features memusage --example statsで確認する
- 実行コマンドは
- Diamond WASM wrapperはdiamond-jsを使用し、wasm bundleは
wasm-optで最適化している - チャートはObservableHQで作成した
1件のコメント
Hacker News の意見
2バイト整数を使ったのなら、キャッシュライン 64バイトが理由である可能性が高い
32個のエントリがちょうどキャッシュライン1本になり、各キャッシュラインがバケット1つ全体を収めて、高コストなメインメモリ転送を減らしたのだろう
基本的には、何かをメモリから何回読む必要があるかを数えるカウンタを入れるのだが、同じようにキャッシュラインを何回退避させる必要があるかを近似できるのか気になる
ワードサイズ(64ビット)、キャッシュ整列されたフェッチサイズ(普通は上で述べた64バイト)、OSページサイズ(4〜16KB)、L1サイズ(コアあたり約80KB)、L2(数MB規模)といった境界で跳ねる
実サービスでCRDTを使っていて、体験が非常によいアプリには何があるのか気になる
Notion がその一つだったと記憶しているが、現実的には Notion で2人が一緒にメモを取るのは、Google Docs に比べるとほとんど使い物にならないレベルだ
タスクと計画のための IDE で、マルチユーザーアプリであり、エンドツーエンド暗号化、オフラインファースト、選択的なセルフホスティングに対応していて、ワークスペース全体が1つのグラフなので CRDT は自然な選択だった
Thymer のすべての操作は、少数のCRDT 変換に集約される。テキストの移動/コピー、「frontmatter」属性の変更、カードのドラッグ、ファイルアップロード、タグ追加が、すべて同じ数種類の CRDT 操作で処理される
初期にライブラリなしで実装したため手間は多かったが、アプリケーション状態が1つのグラフなら、ページ間のテキスト移動、バックリンク付きのページリンク、トランスクルージョンのような機能を、同期の心配なしに作れるので十分価値があった
CRDT はすべてのクライアントが同じ状態に収束することを保証し、本質的に追記専用なので、特定時点のバージョン管理も無料で手に入る
ただし性能のためにいくつか妥協はした。バージョン履歴はデータ量が多すぎるためオフラインでは提供せず、場合によっては最後の書き込み優先の競合解決を使っている
全体としては、特に最初から CRDT を念頭に置いてアプリを設計するなら非常に価値がある。より伝統的な AJAX アプリに後からマルチユーザー機能を付け足す状況なら、CRDT は使わないと思う
CRDT の変更はまず楽観的に適用され、権威あるイベント順序が決まると、すべてのクライアントはいったん最後の共有状態へ戻ったうえで、正しい順序ですべてのイベントを再適用しなければならない
長くオフラインだった場合、数日分の変更を巻き戻して再適用する必要があるかもしれない。ユーザーは裏でどれだけ多くのツリー変換が起きているか知らないが、CRDT はアプリケーション全体の設計に影響する
今日人気のあるほとんどのアプリは、CRDT 変換がまだ十分理解されていなかった時期に設計された
[1] https://thymer.com(ほぼベータ準備完了)
テキストは最後の書き込み優先で、各ブロックのテキストやプロパティは最後の書き込み優先レジスタになっている。ブロックテキスト用の新しい CRDT 形式に取り組んでいる
Notes、Reminders が含まれ、Photos もそうかもしれない。バックエンドの一部として FoundationDB も使われていると、酔った元 Apple SRE からバーで聞いた
Local First Conf の発表も参考になる: https://youtu.be/VLgmjzERT08
何らかの形でロールバックや補正を行うネットワークゲーム全般がこれに近い。ベストエフォート方式にロールバックを代替経路として持たせる形は、広く使われている CRDT の中でもユーザー体験の面で最良の形かもしれない
厳密な学術的意味での CRDT ではない。技術的にはゲーム状態がすべてのクライアントに完全複製されるわけではなく、各クライアントは部分的なゲーム状態だけを受け取るからだ
またゲームクライアントには低遅延の同期が必要で、学術的にはこれを「調整」と見なすこともできる。実際には、クライアントは競合解決やロールバック補正が返ってくる前に、入力結果を確率的にローカルで受け入れてレンダリングする
細かく突っ込む人もいるだろうが、3つ目の例まで見ると共通するテーマが見えてくるはずだ。最も人気があり、使いやすく、よく実装された CRDT は、実際には学術的な規則を破っている
これは過度に学術的な思考モデルの典型的な落とし穴だ。現実のアルゴリズムやデータ型は、学術的な「ルールブック」よりはるかに創造的な場合が多い。たとえば Timsort がそうだ
論文査読ではなく実際のユーザーのための製品を作るなら、過剰なエンジニアリング/学術的な罠にはまるべきではない。学術的な規則を学んだうえで意図的に破り、学者同士にしか有用でないほど厳密に定義された概念を完全実装しようとするより、実際の価値を加えて使いやすくするべきだ
CRDT は強力だが、操作ベースであれ状態ベースであれ、過去の操作や要素の痕跡を残す点が惜しい。
圧縮しても依然として欠点であり、導入をためらわせる部分でもある。
それでもこの議論のおかげで、Dropbox や Syncthing のようなファイルベースのストレージ上に、衝突のないアルゴリズムや細かな衝突解決アルゴリズムを実装できる可能性には興味が湧いた。
私のポスト CRDT プロジェクトである Diamond Types は、基本的には時間が経つにつれて無限に大きくなりますが、オーバーヘッドは通常、これまで入力された文字 1 文字あたり 1 バイト未満です。
保存テキストに LZ4 圧縮を有効にすると、Diamond Types で編集したドキュメントは、編集履歴全体を保存しているにもかかわらず、最終的なドキュメント状態より小さくなることがよくあります。
技術的にこれを解決する方法はいくつか知っていますが、ほとんどのシステムでこれが実際の問題なのかは確信がありません。
yjs を 3D モデリングツールに使っていた人が問題に遭遇したという話は聞きました。オブジェクトをドラッグしている間、マウスがピクセル単位で動くたびに永続的な編集を作っていたためです。
そのような作業には、ほとんどの CRDT ライブラリが対応していない一時的な編集を使うほうが賢明だと思います。
ちなみに Git にも同じ問題があります。リポジトリは時間とともに大きくなり、現代的な CRDT ライブラリを使った場合よりもはるかに速く増大します。しかし、誰もあまり気にしていないようです。
Git では浅いクローンも可能ですが、ほとんど誰もやりません。望むなら CRDT でも同じことができます。
たとえば、すべてのクライアントが X 日付以降の変更を受け取ったと保証できるなら、その日付以前の操作は安全に捨てられます。
この分野の最新研究を活用しようと Fireproof[1] を作っています。
不変データをコンテンツアドレス化すると、各操作が暗号学的に保証された証明や差分に帰着するという追加の利点があります。因果一貫性を強制し、スナップショットに安定した参照を作れます。
つまり、相互作用可能で、オフラインで動作し、損失なくマージされるデータベースをエッジやブラウザで動かしながら、かつては中央データベースやブロックチェーンに期待していた整合性を持たせられるということです。
たとえば署名用の PDF やスマートコントラクトにスナップショット CID を入れて、参照された状態に関する曖昧さをなくせます。
[1] https://github.com/fireproof-storage/fireproof
結局のところ、特定のデータ型やデータベースが従う数学的法則の集合について包括的に語るよりも、具体的な実装を批判するほうが適切です。
2021 年の記事で、Automerge の Rust 実装も入ったようなので、更新されたベンチマークを見ると興味深そうです。
この問題を解く、まったく別の新しいアプローチも持っています。
ベンチマークを更新できれば間違いなくよいでしょう。全部速くなっています。
内容の多くは理解するのが難しかったものの、文章があまりにうまく書かれていて、止まらずに読み進めてしまう珍しい記事です。
以前の関連議論です。
https://news.ycombinator.com/item?id=28017204(3 年前、コメント 151 件)
https://news.ycombinator.com/item?id=33903563(2 年前、コメント 22 件)
https://news.ycombinator.com/item?id=41372833(現在の投稿)
https://news.ycombinator.com/item?id=41373288(現在のコメント)
現在の GitHub Readme[0] を引用すると、そのブログ記事以降も性能がさらに 10〜80 倍向上したそうです。
[0] https://github.com/josephg/diamond-types
なぜ CRDT が遅いのか説明してもらえるとよいです。
この記事は未来を示しているように思います: https://joelgustafson.com/posts/2023-05-04/merklizing-the-ke...
これを見て Y.js や Automerge と比較してみるとよさそうです: https://github.com/canvasxyz/okra-js
数年前にこの記事を書いてから、主要な CRDT ライブラリはすべて数桁規模で速くなりました。
数年前にこの記事を偶然見かけた記憶があります。
本当に面白い記事で、ここ数年で最も好きな記事の一つです。
「なぜ WASM はネイティブ実行より4倍遅いのか?」については、すべての文字列操作が WASM メモリにコピーされ、結果が計算されるとまた JS にコピーし戻す必要があるからだと思っていました
私が間違っているのでしょうか?文脈を誤解しているのでしょうか?純粋に気になります
時間を測る前に全履歴を wasm の中へロードし、wasm コンテキスト自体で実行される Rust 製の内部ループで処理していました。wasm 呼び出しは2回程度だけでした
4倍遅くなった原因は FFI ではなく、アルゴリズムのコード自体が実際に4倍遅く実行されていたことでした
今ベンチマークを回し直すと面白そうです。コンパイラの wasm 出力も改善されていますし、wasm ランタイムも速くなっているはずです。ベンチマークコードはどこかにまだあると思います
別の領域でずっと引っかかっている問題は、マルチスレッド の話が簡単ではなく、ライブラリやツールのサポートも完全ではない点です
ゲームエンジンや ffmpeg、zip のようなユーティリティバイナリをブラウザで動かしたことがありますが、そのせいで非常に遅かったです
WASM インタプリタや JIT を扱ったことはありませんが、翻訳レイヤーを1つ通すより複数のレイヤーを通したほうが良いケースが、どれほど頻繁にあるでしょうか?
高水準コードを WASM や何らかのアセンブリ言語に翻訳すると、高水準コードに含まれていた多くの意図が失われます
低水準コードでは、目的を達成するための言語固有のイディオムの羅列をよく目にしますが、それが実際のマシンに直接対応するものもあれば、そうでないものもあります
現代の x86-64 には、WASM でできることよりはるかに強力な命令が非常に多くあります
もちろんデコンパイラもありますし、WASM JIT が比較的単純なパターンマッチングで良いネイティブコードを生成できるマクロ演算融合のリストがあるかもしれません。おそらくそうではないでしょうし、クロスプラットフォーム最適化はさらに難しいでしょうが
LLVM も完璧ではないので、後処理の最適化器が改善できる簡単な部分は確実にあります。したがって LLVM のネイティブ出力より WASM を速くすることが理論上不可能というわけではありません
しかし、非常によく練られた計画がない限り、あるいは実質的に対象命令セットアーキテクチャがサポートするもののスーパーセットである命令セットを作らない限り、同じ水準の結果を得るのは非常に難しいと思います
私には WASM はむしろサブセットに近いように見えるので、演算を標準化し、リアルタイムに再び合成するのは簡単ではありません。完全に不可能ではないとしても、相当なエンジニアリング上の成果が必要です
直感的に言えば、英語で書かれた本を、まったく異なる言語で、しかも数千語に制限された言語へ翻訳し、それをまた英語へ翻訳しても、まったく同じテキストにはなりません
英語なら一語で済む概念を、段落で説明しなければならない場合が出てきます。元の英語を取り戻すには、あらゆるものについての1:1の翻訳か、2人の翻訳者が合意した「段落→一語」の翻訳リストが必要です