2 ポイント 投稿者 GN⁺ 2024-04-15 | 1件のコメント | WhatsAppで共有
  • WebAssemblyはPhotoshopのような大規模C++プログラムをWebに持ち込む点では成果を上げたが、DOM中心のアプリではJavaScriptとは異なるプログラミングモデルのため普及が限定された
  • ブラウザのWasm GCと参照型サポートはPythonやSchemeのようなガベージコレクション付き言語に機会を開くが、Webでは転送サイズがそのまま採用障壁になる
  • Goの単純なWasmプログラムは2MB、importを追加すると10MB以上になることがあり、Pyodide REPLは約20MBをダウンロードするため一般的なWebアプリには負担が大きい
  • Hoot SchemeコンパイラはGC対応Wasmをターゲットとし、最小の「main」コンパイル単位を約70KBまで削減しており、補助コンパイル単位は1KB未満も可能
  • 効果的なツリーシェイキングは参照されない関数を消すだけではなく、フロー解析と標準ライブラリ設計がかみ合うコンパイラの問題である

WebでWebAssemblyが成果を上げた領域

  • WebAssemblyはWebで当初期待されたほど広く普及してはいないが、特定の領域では限定的な成功を収めている
  • 代表例はPhotoshopのように大規模なC++プログラムをWebに持ち込んだケースである
  • Figmaも5年前のWasm事例として言及されるが、現在はWasmをあまり前面に出していない
  • C++またはRustからコンパイルされた小さなNPMライブラリが内部的にWasmを使っている場合は多い
  • Blazorは社内向け企業アプリの一部で使われている可能性はあるが、マーケティングで誇張されていた余地もある
  • Unreal Engineの3D FPSデモは5年以上前の主要リリースをベースにした実験であり、現在のUnreal 5はWebAssemblyターゲットをサポートしていない

DOM中心アプリでWasmが伸びなかった理由

  • WebAssemblyはWeb以外の環境では成果があり、Webプラットフォームでも重要性が高まる可能性はあるが、Webでは幻滅期をようやく抜けつつある段階と見なせる
  • WasmはJavaScriptが得意でない処理や、クライアント・サーバー間で実装共有が必要な処理で強みを持つ
  • DOM中心のアプリではWasmは成功しなかった
    • wordpress.comのフロントエンドをWasmで書き直そうという話は出てこない
    • Webの主要なプログラミングモデルは動的型付けとガベージコレクションを持つJavaScriptである
    • WebAssembly 1.0は静的型付けと線形メモリを中心に設計されていた
    • WasmからDOMにアクセスする作業は、熱心なWasm支持者しか耐えられないほど煩雑だった
  • C#のような言語ではガベージコレクタを一緒に配布しなければならず、この点がC/Rust以外の言語でWasm採用が進まない要因となった

Wasm GC以後も残る転送サイズの問題

  • ブラウザは今後数か月のうちに参照型とガベージコレクションのサポートを提供する予定である
    • ChromeとFirefoxはすでにWasm GCを提供している
    • SafariもAsumu Takikawaの作業によりそう遠くないと見られる
  • Wasm GCは、より多くの言語がWebAssemblyをサポートするようツールチェーンを更新させる変化である
  • Web向けWasmが成功するには、コンパイラが小さなコードを生成できなければならない
    • 言語ツールチェーンが転送サイズで数KB級の有用なWasmファイルを生成できれば有利である
    • そうでなければ、過度な期待や囲い込まれたユーザー層に頼るしかなく、次の解決策が見つかるまで不安定な均衡にとどまりうる
  • JavaScriptエコシステムでは、配信サイズと肥大化を抑えるためのツール産業がすでに大きい
    • esbuildのようなバンドラは複数のJSモジュールを1つのファイルにまとめる
    • 使用される関数やデータ型だけを含めようとする
    • minificationのように名前を短くするサイズ削減戦略も適用する

ツリーシェイキングという名前の落とし穴

  • ツリーシェイキングには、あるページで必要なコードだけを残して残りを振り落とすという視覚的比喩がある
  • この比喩ではモジュールが枝、定義が葉のように想像されるが、実際の木は幹をつかんで揺らしても必要な枝と不要な枝を教えてはくれない
  • 名前そのものが不要コードを削除する方向で考えさせるが、アルゴリズムの観点では必要なコードだけを保持する不動点を見つけると考えるほうが適切である
  • それでもツリーシェイキングは印象的な名前なので、園芸学的にもアルゴリズム的にも不正確であっても使われ続けている

厚いランタイムが生むサイズ障壁

  • ランタイムが厚い言語では、最大限のツリーシェイキングは大きな優先事項ではなかった
  • GoのWebAssemblyサポートでは、最も単純なプログラムでもgolang wikiによれば2MBである
    • importを追加すると10MB以上になることがある
  • PythonのWebAssembly移植であるPyodideのREPL例は約20MBのデータをダウンロードする
  • こうしたサイズは技術デモや非常にリッチなアプリケーションには問題ないが、一般的なWeb開発の選択肢になるのは難しい

代替ツールチェーンとプラットフォーム特化実装

  • Goの組み込みWasmサポートとPyodideは上流ツールチェーンから派生したものであり、サーバーではバイナリサイズがそれほど重要でない場合もある
  • 小型デバイスをターゲットにすると別実装が現れる
  • TinyGoのWasmバックエンドは1KB未満まで下げられるようである
  • こうした代替ツールチェーンは制約や癖を伴うことが多い
  • DOM環境で動くWasmターゲットのPythonプログラムは「ネイティブ」Pythonプログラムとは異ならざるをえない
  • ツールチェーン作者は同じ言語を提供しようとするが、標準ライブラリ実装は異なりうる
  • ClojureScript開発者も、可能であればClojureとの違いを説明する文書をなくしたいはずであり、WasmがClojureScriptの実用的ターゲットになればその可能性が生まれる

Hoot Schemeのツリーシェイキング方式

  • GC対応後のWasmはPythonのような言語でDOMプログラミングを考えられるようにするが、大衆的な利用には小さなモジュールが必要である
  • Hoot Scheme compilerはGC付きWasmをターゲットとする
    • 現在の最小「main」コンパイル単位は約70KBである
    • さらに小さいサイズを目指している
    • 例外ハンドラのようなランタイム機能をmainモジュールからimportする補助コンパイル単位は1KB未満になることもある
  • Hootコンパイラはユーザーコードの前にpreludeを付加する
  • ツリーシェイキングはいくつもの段階で起こる
    • 部分評価は未使用の束縛を副作用だけ評価したうえで削除できる
    • fixing letrecも同様の作業を行う
    • CPSはプログラムを頻繁に走査し、参照された関数、値、制御フロー辺だけをたどる
    • 明示的なdead-code eliminationパスは、他の最適化の後に生じうる未使用かつ副作用のない代入を削除する
    • rawに近いWebAssemblyで書かれた標準ライブラリの定義は、必要な場合にのみ結果バイナリに含まれる

簡単に消せるものと消しにくいもの

  • 関数やクロージャのような手続き定義は比較的扱いやすい
    • コードが参照する関数だけを含めればよい
    • Schemeのような言語では、これだけでもかなりの効果がある
  • すぐに見えてくる難点は3つある
  • letrec* の評価モデル

    • prelude定義のスコープは再帰的だが順序がある
    • 束縛値は先に定義された値を呼び出したり参照したりでき、後で定義される値をキャプチャすることもできる
    • 束縛値の評価において、後になって定義される値を参照しなければならないならエラーになる
    • 手続きではたいてい問題にならないが、非手続き定義ではコンパイラが「先行する束縛だけを参照する」という性質を証明できないことがある
    • この場合、fixing letrec reloadedアルゴリズムはset!された束縛を残す可能性があり、それを除去するには繊細なDCEパスが必要になる
  • レコード型のvtable

    • 一部の非手続き定義はレコード型である
    • レコード型は、レコードの出力方法やインスタンス検査方法などを保持するvtableを持つ
    • vtableのコールバックは実際には使われなくても多くのコードを生き残らせる可能性がある
  • 多相的な出力関数

    • displayのような多相関数は、必要となるコード範囲を大きく広げる
    • 文字列を出力するためにdisplayを呼ぶと、バッファ付きI/O機構全体を引き込む
    • displayは何でも出力できるため、bitvectorやpairなど様々なケースのコードも一緒に引き寄せることがある
    • 文字列だけを使うwrite-stringを呼べば汎用データ出力コードは避けられるが、それでもポートのような一般的なバッファ付きI/O機構は含まれる

最適なツリーシェイキングはフロー解析の問題

  • 最適なツリーシェイキングは結局のところフロー解析の問題である
  • プログラムにbitvectorが絶対に存在しないなら、display内でbitvectorを処理するコードはデッドコードになりうる
  • それを知るには、displayがどの種類の引数で呼ばれるのかを把握する必要があり、そのためには高水準フロー解析が必要である
  • Pythonでは問題はさらに難しくなる
    • オブジェクト指向ディスパッチは高階プログラミングなので、foo.barが何を意味するかはfooが何であるかに依存する
    • Pythonの名前解決はSchemeよりさらに動的で、__getattr__のようなメソッドがいたる所で使われうる
    • 実際には、フロー解析でこうした動的な名前解決を除外できるかもしれない
    • Pythonのツリーシェイキング対象は、レキシカル束縛を持つ大きな項ではなく、モジュール群の複雑な集合である
    • これはJavaScriptに似ているが、Pythonには確立したツリーシェイキング向けバンドラのエコシステムがない

Web向けWasm言語ツールチェーンの条件

  • Wasm GCはJavaScript以外の言語でDOMプログラミングを可能にしうる
  • 大衆的利用につなげるには、生成されるWasmモジュールが小さくなければならない
  • 各言語ツールチェーンには相当な投資が必要である
  • こうした投資は、実験的なツリーシェイキングアルゴリズムを含む代替ツールチェーンとして現れることが多い
  • 代替標準ライブラリは、ツリーシェイカがよりうまく機能するよう設計される必要がある

1件のコメント

 
GN⁺ 2024-04-15
Hacker News のコメント
  • openEtG の Wasm blob(カードゲームエンジン)を 400KB 未満に保ちながら、カードテキスト生成など多くのロジックを Wasm に移し、Rust で書いた。
    サイズを減らすには、浮動小数点の代わりに固定小数点演算を使い、ハッシュマップの代わりにベクターへ移行し、文字列を避け、talc のような小さなアロケータを使い、依存関係を減らすといった管理が必要だった。
    randfxhash だけを使っているが、rand も取り除けそうで、fxhash はゲーム状態のハッシュで非同期化の有無を確認するためだけに使っている。
    ジェネリックのインスタンス種類も減らし、Vec がすでにあるので Box<[i16]> のような型を追加で引き込まないようにした。浮動小数点とハッシュマップをなくしたことも型の多様性を減らす助けになった。
    アルゴリズムもサイズを考慮して設計しており、たとえばビットパックしたルックアップテーブルで、攻撃力の低いクリーチャーほど多く攻撃する adrenaline メカニズムをエンコードしている。
    圧縮されていない値を保存するコストとデコードロジックのコストを比較し、AI 評価は WebAssembly では 128 より 64 のほうが効率的にエンコードされるため、6ビット固定精度を使っている。
    ターゲティングの仕組みも、以前は各述語を enum にし、AND/OR を式スライスとして持つ AST 形式だったが、現在はポーランド記法で式を 32 ビット整数にエンコードし、AND/OR は 2 ビット、述語は 6 ビットに収めている。
    ここでは逆ポーランド記法よりポーランド記法のほうが良かった。AND/OR の短絡評価が可能だったからだ。

    • Wasm には浮動小数点型が標準であるのに、固定小数点演算でかなり容量を節約できたという点が興味深い。
      仕事で最大解像度要件が分かっている問題、たとえばミリメートル未満の位置精度が不要な場合に固定小数点が役立つのか考えているので、関連する話をもっと聞きたい。
    • さらに役に立ったのは binaryen の wasm-opt を使うこと。
      Wasm サイズを安定して約 20〜30% 減らしてくれるようだ: https://github.com/WebAssembly/binaryen
      ブラウザに Wasm バンドルを提供する場合は、Brotli 圧縮を使い、Web サーバーが Brotli 圧縮ファイルを使うよう設定するのもよい。
      nginx なら 1 行の変更で可能で、Brotli は Wasm バンドルサイズを約 3 分の 1 にしてくれ、gzip よりはるかに優れている。
    • C++ の深層強化学習ライブラリ RLtools を作り、Wasm の例も作った(https://rl.tools)が、バイナリサイズをまったく気にしていなかったのに全体で 200〜300KB 程度だった。
      深層学習の順伝播/逆伝播、強化学習アルゴリズム、動力学シミュレーションまで全部含めたサイズだ。
      今でも負担になるほどではないが、どれくらいさらに小さくできるのか気になるので、近いうちに削ってみたい。
    • JavaScript はすべての数値に double を使うと思っていたが、Wasm はこの点でまったく違うのか気になる。
      浮動小数点を避けて固定小数点演算で容量を節約するという話がどう成り立つのか理解したい。
    • 実際にどれくらい容量を節約できたのか、数字があれば見てみたい。
      特に 6 番のような VecBox のように使う方法が大きな節約につながるとは思えない。
  • ツリーシェイキングという名前は、かなり不適切な呼び名のように思える
    Virgil コンパイラはこれを「到達可能性解析」と呼び、コンパイルモデルに組み込んでいる
    コンパイラはプログラムとライブラリコードをパースし、型検査し、初期化コードを実行するが、その後はメインのエントリポイントから探索して到達可能なコードだけを解析し、最終バイナリに入れる
    ランタイムシステムなしで単一の main 関数だけがあるプログラムも問題なく生成でき、ランタイムシステムはスタックトレースとガベージコレクションにだけ必要なので、望めば省略できる

    • アプリケーション配布ツール名としての Treeshaker は Lisp に由来する可能性がある
      最初に見つけた事例は 1992 年の Lucid Common Lisp 4.1 の Treeshaker ツールで、これは UNIX 向けの商用 Common Lisp 実装だった
      Lucid CL には、実行中の Lisp ヒープの保存済みメモリダンプであるイメージという概念があり、アプリケーションはイメージとランタイムで構成されていた
      イメージは通常、メモリ内のほぼすべてのコードとデータを含むため、配布用により小さなイメージを作りたくなり、Treeshaker はイメージを保存する前に「未使用」と判断されたコードとデータを削除していた
      到達可能な Lisp データとコードのグラフから接続を刈り込み、GC や特殊なコードがゴミを回収してメモリを減らしたうえで、より小さなイメージとしてダンプする、という仕組みである
      したがって Treeshaker はコンパイラツールではなく、Lisp ヒープから未使用のコードとデータを削除するツールだった
      基本の Lisp イメージにはコンパイラ、インタプリタ、REPL 実装まで含まれていたため、実行中のプログラムを割り込ませて REPL に入ると、イメージから復元されたヒープ内のすべてのコードを依然として利用できた
      そのため、コンパイラや REPL まで削除することに意味があった
    • 一般に適切な用語は デッドコード除去(dead-code elimination) である: https://en.wikipedia.org/wiki/Dead-code_elimination
      到達可能性解析は、どのコードを削除できるか判断するためによく使われるが、解析自体がコードを削除するわけではなく、削除は後続の段階で行われる
      「ツリーシェイキング」は通常、関数単位の除去を示唆する一方、デッドコード除去は条件式の分岐除去のように、はるかに細かい粒度でも可能で、さまざまな静的解析に基づき得る
    • ソフトウェアには不適切な名前が多いが、個人的には ツリーシェイキング は優れた名前だと思う
      初めて見たとき、追加で調べなくてもすぐに意味が理解できた
      木を揺らして、緩く付いているものを落とすということで、ここでは使われていないパッケージがツリーから「揺すられて落ちる」という意味だと明確だった
    • ツリーシェイキングが植物としての木を指していると思ったことはなく、データ構造としてのツリーを思い浮かべた
      ソースコードのダイアグラムを物理的なオブジェクトとして想像すると、揺らしたときにルートから到達できないものが落ちていく
      到達可能性解析と大きく異なるわけではなく、片方の表現のほうが空間的な推論をより呼び起こすだけだ
    • コードが大量にあり、それがツリーである
      一部のコードはエントリポイントという幹につながっていない
      ツリーシェイキングは、つながっていない部分、つまり緩い葉や枯れ枝を取り除く
  • 言語のコンパイラツールチェーンが、ネットワーク転送ベースで数 KB に満たない有用な Wasm を作れるなら、新しい可能性が開ける
    非常に小さなバイナリは Wasm の新しいユースケースを開くだろうし、WasmGC は間違いなく役に立つ
    Java と Kotlin も現在では 2〜3KB 程度でかなりうまくやれる: https://developer.chrome.com/blog/wasmgc, https://twitter.com/bashorov/status/1661377260274720770
    ただし、どの API を使うかによって大きなコードが付随してくることがあるので注意が必要だ
    それでもこれらの言語は WasmGC のおかげで、メモリ管理コード数 KB をバンドルに入れる必要がなく、コードサイズの面ではすでに C++ や Rust よりかなり有利なほうだ

    • WasmGC が Rust にも役立つのか気になる
      JavaScript オブジェクトを扱うときには役立つかもしれないし、効率は劣る代替メモリアロケータとしても使えそうだ
      それでも Wasm における標準の Rust アロケータは、ほとんどの場合で十分に良い可能性が高い
      サイズ最適化を始め、wasm-opt を使い、Brotli で圧縮すれば、ダウンロードベースで 100KB 未満に膨大な量のコードを入れられる
      Wasm 100KB のコストをバンドルされた JavaScript 100KB と直接比較するのは誤りで、JavaScript はパースと初期化が何倍も遅い
      ダウンロード時間は実際のコストだが、初期表示時間では 100KB の JavaScript より 100KB の Wasm のほうがはるかに良い
      それでも小さいほど良く、Java、Kotlin、C#、Python、Go などが Web アプリケーションで実用的な言語になる点には期待している
      実際のアプリケーションサイズがどうなるのかも気になる
      最大の違いはフレームワーク設計から生まれそうで、仮想 DOM の差分比較は Svelte、SolidJS、Rust の Leptos のようなリアクティブコンポーネントライブラリより、常に複雑で遅くならざるを得ない
      WasmGC がどこでもサポートされるようになれば、性能には言語よりも Web フレームワークの選択 のほうがはるかに大きな影響を与えそうだ
  • 「Wasmによって、JavaScript以外の言語でのDOMプログラミングが考えられるようになる」という話が本当に正しいのか疑問です
    Rustのような言語でDOMを操作するには、結局JavaScript側で実行される呼び出しをシリアライズするバインディングが必要だと理解しています
    現在の形のWasmでは、依然としてJavaScriptに縛られているのではないかと思います

    • 先ほどの「GCがあれば」という条件を含めることが重要です
      理論上は、いまやランタイムからDOM関数を取り込み、DOMオブジェクト参照で呼び出して、JavaScriptを迂回しランタイムを直接呼び出せます
      実際に可能かどうかは確信がありませんが、GCは少なくともその地点へ向かうための前提となるメカニズムを提供します
    • 最近Rust向けのLeptosを使ってみましたが、WasmでWebフロントエンドを作る非常に高速なフレームワークで、正直よさそうに見えます
      基本的にはSolidJSに似ています
      #[component] fn App() -> impl IntoView { let (count, set_count) = create_signal(0); view! { "Click me: "{move || count()} } }
      JavaScriptに比べるとWasmのサイズのオーバーヘッドはありますが、深刻ではありません
      wasm-optとBrotli圧縮の後、このカウンターアプリのWasmバンドルは37KBで、Reactと同程度の範囲に収まりつつ、実行後はずっと高速です
      直接のDOM操作は試していませんが、一般的なコンポーネントにはよさそうです
    • 私の理解では、WASIの意図の一つはDOMへ直接アクセスするAPIを提供することです
      たとえば、Rustで実装されたDOMを、Rustで書かれたWasmモジュールからJavaScriptの実行なしに使える形です
      ただしDOM APIにはJavaScriptの意味論に合わせて仕様化されている部分があり厄介なので、まずはHTTPリクエスト、TCPソケット、ファイルシステムアクセスのようにJavaScriptの遺産が少ないものから進めているようです
  • 「デッドコード削除」という用語は昔からあったのに、なぜツリーシェイキングという表現が生まれたのか気になります

    • 「デッドコード削除」は通常、関数内部で絶対に到達しないコード片を捨てる、小規模なコンパイラ最適化を指します
      「ツリーシェイキング」は、呼び出されないモジュール全体や関数を捨てる全プログラム解析を指します
      概念的には同じですが、コンパイラ作者はたいてい別々に実装する必要があるため、2つの名前があるのは有用です
    • どの単語が生き残って主流になるかは、古いかどうかとはあまり関係がありません
      ツリーシェイキングは連想しやすく、「デッドコード削除」より言いやすく親しみやすいので、より人気のある用語になったのだと思います
      実際にどちらの用語が先だったのか調べてみると、「dead-code elimination」の最も早い使用例は1973年の論文で見つかりました: https://research-repository.st-andrews.ac.uk/bitstream/handle/10023/22636/NicholasAlexandrakisMScThesis1973_original_C.pdf?sequence=1
      Google Scholarでは、コンピューティング分野における「tree shaking」や「tree shaker」の用例は見つからず、ほとんどが柑橘類の木など樹木に関する内容でした
      最も早い議論と思われるものはcomp.lang.lispの投稿でした: https://groups.google.com/forum/#!topic/comp.lang.lisp/pspFr1XByZk
    • 本当にLispの文脈に由来するなら、実行コードだけでなく不要なデータとメタデータの削除も同じ比重で扱うからでしょう
    • デッドコード削除という表現のほうがずっと良いです
    • より生き生きした表現で、「デッドコード削除」より口語的に聞こえるので、広まった理由を理解するのは難しくありません
  • ツリーシェイキングの比喩は、一部の果樹の収穫方法から来ているのではないかと思います
    木を揺らすと熟した果実が落ちます
    ただし果実の収穫では落ちたものが欲しいのに対し、シリアライズされたイメージを保存するときは落ちたものを捨てるという点で、あまり良い比喩ではありません

    • 通常、揺らして収穫するのはナッツ類です
      果物はより繊細で、遠くに落ちすぎると傷むため、たいてい手で摘みます
      揺らす工程はかなり荒っぽいものですが、木には害を与えません
  • 私の哲学は、JavaScript ではアルゴリズムと設計の単純化によってサイズと性能を懸命に最適化し、力任せの計算が必要なコードに使うためのバンドルサイズと CPU の余裕を作ることです
    ClubCompy プロジェクトでは、ローカルストレージ上に FAT ファイルシステムを実装するために Wasm を使っているのですが、計算コストが非常に大きいことが分かりました
    今年後半にピクセル単位で正確なスプライト衝突判定を戻すときにも Wasm を使う予定です
    最初の実装は純粋な JavaScript で、画面上の 256 個のスプライトが互いに衝突するとフレームレートが 1fps 未満に落ちました
    ワーカースレッドで実質的にただ同然に処理し、性能への影響なしにできると見ています

    • ピクセル単位の衝突判定は、初めて聞くと良さそうに思えますが、実際にプレイすると不快に感じることがあります
      ほとんどの 2D ゲームが矩形の当たり判定ボックスを使うのには理由があり、プレイヤーが衝突するかどうかを予測しやすいからです
      ピクセル単位の衝突では、同じ移動でもアニメーション周期の位相によって衝突したりしなかったりします
      以前は常にできていた動きが、アニメーションのタイミングが悪く噛み合って失敗すると、気分がよくありません
      そのうえ、ピクセル単位が常に現実的というわけでもありません。スプライトの細かなディテールは、布や髪のように実際には硬い衝突を起こさない要素かもしれません
      スプライトがフレームごとに複数ピクセルずつ動くことも多いため、個々のピクセル衝突は互いにすり抜ける可能性を高めます
      単純で予測しやすい衝突判定がたいてい最善です
    • 256 個の衝突で 1fps なら、プログラミング言語ではなく アルゴリズムがボトルネックです
      1fps は衝突が 2 万件ほどあるときに出るべきフレームレートです
      ピクセル単位なら、メモリに十分な余裕がある場合、単純な方法として競技場全体のオフスクリーンキャンバスをレンダリングし、各スプライトをそれぞれ異なる色のステンシルとして描いてから、それを検査できます
      線形時間で、別途分割も不要です
      ただし、アンチフィンガープリンティング対策によってキャンバスデータの下位ビットがノイズに置き換えられることがあるため、上位ビットを使う必要があるかもしれません
  • この記事は正しいことを言っている。Wasmにはコードサイズの問題がある
    ブラウザではサイトの起動前にすべてのコードをダウンロードする必要があるため問題であり、サーバーレスアーキテクチャでも、クライアントが待っている間にコードがコールドストレージから特定のサーバーへ必要に応じてロードされるため問題になる
    ツリーシェイキングは助けにはなり得るが、漸進的な最適化にとどまるように思う
    根本的にWasmプログラムが肥大化する理由は、それぞれの言語ランタイムと標準ライブラリを丸ごと持ってくる必要があるからだ
    逆にJavaScriptは、実装と基本ライブラリをブラウザが提供している
    とはいえ、ブラウザがすべての言語ランタイムをあらかじめ載せておくことはできないと思いがちだが、別のアプローチも考えるべきだ: 共有ライブラリと動的リンク
    WebAssemblyは動的リンクをサポートしており、複数のWasmモジュールを同時にロードして相互に呼び出せる
    しかし多くのWasmツールチェーンはこれをサポートしようとせず、プログラム全体と言語ランタイムを1つの巨大なモジュールへ静的リンクするように設計されている
    Pyodide(Wasm上のCPython)は反例で、現在は動的リンクを念頭に置いて設計されている
    Cloudflare Workersが最近Pythonをファーストクラスサポートとして追加できたのも、まさにこのおかげだ: https://blog.cloudflare.com/python-workers
    Workersプラットフォーム全体の技術リードの立場から見ると、同じマシンで実行されるすべてのWorkersがコンパイル済みのPyodideランタイム1つを共有するため、各Workerごとに別々にロードする必要がない
    動的リンクがより広くサポートされれば、ブラウザが人気のある言語ランタイム、さらには人気ライブラリをあらかじめロードしておき、そのランタイムを必要とするすべてのWebページが同じ読み取り専用コードのコピーを共有する構造を考えられる
    これらのランタイムは依然としてサンドボックス内で実行されるため、ブラウザが信頼する必要はなく、提供するだけでよい
    こうすれば、ブラウザのメンテナーが言語実装を完全に検証したり悩んだりしなくても、JavaScript以外の言語を「組み込み」でサポートするブラウザを作れる

    • ブラウザが複数の人気ランタイムやライブラリをあらかじめロードしてWebページ間で共有するという構想は、以前よくあるJavaScriptフレームワークを共通CDNで提供すれば、大半のブラウザキャッシュにはすでに入っているのでダウンロード不要で、フレームワークのサイズはそれほど重要ではない、というアイデアに似て聞こえる
      詳しくは知らないが、その方式はうまく機能しなかったと理解している
      ライブラリのバージョンが多すぎて個々のバージョンは実際には広く使われず、その後プライバシー上の懸念から、ブラウザはサイトやオリジンごとにキャッシュを分離する方向へ進んだ
      キャッシュを念頭に置いたものではないのかもしれないが、これは技術的問題というより社会的問題に近い厄介な問題だ
      より多くの言語ランタイムをブラウザの標準サポートに入れると、新しいブラウザの参入障壁がさらに高くなり、人々が望むすべてのライブラリやランタイムをサポートすることもできない
      各自に持ってこさせてキャッシュに期待するなら、以前JavaScriptライブラリのキャッシュで経験した問題をどう避けるかが残る
    • 限定的な動的リンクサポートだけでも、かなり大きな効果がありそうだ
      例えばGoの共有ライブラリに、ランタイムと多くのプログラムが使う標準ライブラリの中核部分を入れられる
      アプリ内部での動的ライブラリサポートがなくても、すべてのGoプログラムのサイズを減らせるし、言語ランタイムは空間最適化を過度にしなくてよい
      すでにロードされており、どれかのプログラムが関数を1つでも使えば無駄な領域ではないからだ
      こうなると、その言語のプログラムにおけるサイズ最適化のコストモデルが変わる
      含まれている標準ライブラリ関数は、その言語を使った瞬間に事実上無料になるので、そのまま使えばよい
      ただし、よく使われるライブラリやフレームワークでも同じ問題が繰り返される
      Cloudflareで実行する場合は、Go向けのCloudflare標準ライブラリも共有したくなるだろう
      問題は、言語がランタイムと同じ速度では進化しないという点だ
      複数の言語バージョンのサポートが制限されたり、共有ライブラリが時間とともに積み上がってアプリ間の共有効果が薄れたりする可能性がある
      JavaScriptは「選択肢がない」というバージョンモデルを持っており、強い後方互換性と、ときにはポリフィルを要求する
      他の言語にはあまり適さないかもしれない
      ランタイムが本当にサイズを減らしたいなら、プラグインの多様性を制限すればよい
      不満はあったが、「JavaScriptを使わなければならない」という方式はブラウザではかなりうまく機能していた
      WebAssemblyベースの言語がそこまで多様である必要はないかもしれず、バベルの塔のような状況では、多様性にはコストが伴う
    • プロファイルベースのツリーシェイキングで、アプリケーションに必要なコードだけを含む小さなモジュールを作り、あまり使われない機能は動的リンクで必要になったときに取得する方式が可能なのか気になる
      実運用の情報をもとにツリーシェイキングを行えば、精巧な静的解析アルゴリズムを実装しなくても、デッドコードやほとんど使われないコードを多く削り落とせそうだ
    • ブラウザが人気ランタイムを事前に受け取る構造は、結局ブラウザに何百ものランタイムバージョンがダウンロードされ、それぞれのバイナリが1つのサイトでしか使われずにキャッシュを埋める危険がある
    • 全体としては同意するが、Wasmでも純粋計算ではないことをするには、ブラウザ組み込みのJavaScriptをかなり多く使うことになる点は指摘しておきたい
      特にHootのような文脈では、appendChildのようなものはSchemeの中から呼び出す外部関数だ: https://spritely.institute/news/building-interactive-web-pages-with-guile-hoot.html
      理論上は、どのWasm環境でもJavaScript標準ライブラリの多くをこの方式で使える
  • Zigはこうした用途に完璧だ
    個人的には、Wasmファイルサイズが100KB未満なら重要な要素ではなく、MBを超えると重要になると見ている
    組み込みGCは一部のアプリには重要だが、すべてではなく、WebアプリはGCなしで作るのが最善だ
    Wasmを使うアプリが成功するうえで最も重要な要素は、依然として性能上の利点だ

  • Cloudflare PagesでBlazorアプリを動かしているが、ダウンロードは速く性能も良い一方で、ロード時間がひどい
    .NETでは解決不可能だと思っていて、根本的な問題はオブジェクト指向言語が設計上、すべてが絡み合うようになっている点のように思う
    また、JavaScriptに投入された資金規模と競争するのは難しく、JavaScriptにはコピー&ペーストが言語機能のように備わっていて、チートキーのようだ
    3つ目に、Blazorでも依然としてJavaScriptとその方面のスキルが必要で、これが主な問題だと見ている

    • それはオブジェクト指向の問題ではない
      その時代の言語が多くのユースケースでリフレクションに依存していたことが問題だ
      .NETの大きな部分でこうしたユースケースをなくし、未使用コードの削除に安全だと示すために、人々が懸命に取り組んでいる
      リフレクションでメソッドが呼ばれる可能性があると、何を安全に削除できるのか判断しにくい
      Foo.Bar()を呼んでいる箇所が見当たらなくても、誰かがReflection.getClass(someClass).runMethod(someVar)を実行していて、その変数が"Foo""Bar"に設定されていたらどうするのか
      たとえばDartは事前コンパイルされたアプリでリフレクションを許可しておらず、そのおかげで未使用コードを安全に削除できる: https://docs.flutter.dev/resources/faq#does-flutter-come-with-a-reflection-mirrors-system
      Dartはオブジェクト指向言語だが、ランタイムでのコード生成とランタイムリフレクションを避け、コンパイル時コード生成を選んでいる
      .NETもこの方向に進んでいるが、一晩でできることではない
      ただし他の人たちが言っているように、非JavaScript言語には、ブラウザのJavaScriptランタイムに含まれる標準ライブラリ機能のうち使用する部分を自前で同梱して送らなければならないという問題もある
    • Blazorはこの面では優れていない
      現在のパッケージングモデルは、Monoの上にWasmがパッケージされるという制約のため、.NETトリミングの能力を十分に活かせていない
      実際にどこまで小さくできるかを見るには、AOTで通常のアプリケーションをビルドしてみるほうがよく、そうすると小さなバイナリが得られる
      dotnet/runtimelabのNativeAOT-LLVM Wasmターゲットの実験的サポートは、はるかに小さいバンドルサイズとはるかに良い性能を提供するが、まだdotnet/runtimeではなくdotnet/runtimelab配下にあるため、いつ使えるようになるかは分からない