WebAssembly ガベージ コレクション(WasmGC)を Chrome でデフォルトで有効化

プログラミング言語には、ガベージ コレクションを使用するプログラミング言語と、手動のメモリ管理が必要なプログラミング言語の 2 種類があります。前者の例としては、Kotlin、PHP、Java などがあります。後者の例としては、C、C++、Rust などがあります。一般に、高水準プログラミング言語には、ガベージ コレクションが標準機能として含まれています。このブログ投稿では、このようなガベージ コレクションを行うプログラミング言語と、それらを WebAssembly(Wasm)にコンパイルする方法について説明します。そもそも、ガベージ コレクション(GC とも呼ばれる)とは何でしょうか。

対応ブラウザ

  • Chrome: 119.
  • Edge: 119.
  • Firefox: 120。
  • Safari: サポートされていません。

ガベージ コレクション

簡単に言うと、ガベージ コレクションとは、プログラムによって割り当てられたメモリが参照されなくなったメモリを再利用することです。このようなメモリはガベージと呼ばれます。ガベージ コレクションの実装にはさまざまな戦略があります。そのうちの 1 つは参照カウントで、その目的はメモリ内のオブジェクトへの参照回数をカウントすることです。オブジェクトへの参照がなくなったら、使用されなくなったとしてマークされ、ガベージ コレクションの対象になります。PHP のガベージ コレクタは参照カウントを使用します。Xdebug 拡張機能の xdebug_debug_zval() 関数を使用すると、内部を覗くことができます。次の PHP プログラムについて考えてみましょう。

<?php
  $a= (string) rand();
  $c = $b = $a;
  $b = 42;
  unset($c);
  $a = null;
?>

このプログラムでは、文字列にキャストされた乱数を a という新しい変数に割り当てます。次に、2 つの新しい変数 bc を作成し、a の値を割り当てます。その後、b を数値 42 に再割り当てし、c の設定を解除します。最後に、a の値を null に設定します。プログラムの各ステップに xdebug_debug_zval() のアノテーションを付けると、ガベージ コレクタのリファレンス カウンタが動作しているのを確認できます。

<?php
  $a= (string) rand();
  $c = $b = $a;
  xdebug_debug_zval('a');
  $b = 42;
  xdebug_debug_zval('a');
  unset($c);
  xdebug_debug_zval('a');
  $a = null;
  xdebug_debug_zval('a');
?>

上記の例では、次のログが出力されます。各ステップの後に、変数 a の値の参照数が減少することがわかります。これは、コード シーケンスを考えると合理的です。(もちろん、乱数は異なります)。

a:
(refcount=3, is_ref=0)string '419796578' (length=9)
a:
(refcount=2, is_ref=0)string '419796578' (length=9)
a:
(refcount=1, is_ref=0)string '419796578' (length=9)
a:
(refcount=0, is_ref=0)null

ガベージ コレクションには、サイクルの検出など、他にも課題がありますが、この記事では、参照カウントの基本的な理解があれば十分です。

プログラミング言語が他のプログラミング言語で実装されている

最初から作られたと感じるかもしれませんが、プログラミング言語は他のプログラミング言語で実装されています。たとえば、PHP ランタイムは主に C で実装されています。GitHub で PHP ソースコードを確認できます。PHP のガベージ コレクション コードは主に zend_gc.c ファイルにあります。ほとんどのデベロッパーは、オペレーティング システムのパッケージ マネージャーを使用して PHP をインストールします。ただし、デベロッパーは ソースコードから PHP をビルドすることもできます。たとえば、Linux 環境では、ステップ ./buildconf && ./configure && make で Linux ランタイム用の PHP がビルドされます。ただし、これは、PHP ランタイムを他のランタイム(Wasm など)用にコンパイルできることも意味します。

言語を Wasm ランタイムにポートする従来の方法

PHP スクリプトは、PHP が実行されているプラットフォームから独立して同じバイトコードにコンパイルされ、Zend Engine によって実行されます。Zend Engine は、PHP スクリプト言語のコンパイラとランタイム環境です。Zend コンパイラと Zend エグゼキュータで構成される Zend 仮想マシン(VM)で構成されています。C などの他の高水準言語で実装されている PHP などの言語には、Intel や ARM などの特定のアーキテクチャをターゲットとする最適化が一般的に含まれており、アーキテクチャごとに異なるバックエンドが求められます。この文脈において、Wasm は新しいアーキテクチャを表しています。VM にアーキテクチャ固有のコード(JIT コンパイルや AOT コンパイルなど)がある場合は、デベロッパーは新しいアーキテクチャの JIT/AOT 用にバックエンドを実装します。このアプローチはかなり理にかなっています。多くの場合、コードベースの主要部分は新しいアーキテクチャごとに再コンパイルするだけでよいためです。

Wasm は低レベルであるため、同じアプローチを試すことは自然なことです。つまり、パーサー、ライブラリ サポート、ガベージ コレクション、最適化ツールを含むメインの VM コードを Wasm に再コンパイルし、必要に応じて Wasm 用の JIT または AOT バックエンドを実装します。これは Wasm MVP 以降可能であり、多くの場合、実際にうまく機能します。実際、WordPress PlaygroundWasm にコンパイルされた PHP で動作しています。このプロジェクトについて詳しくは、WordPress Playground と WebAssembly を使用してブラウザ内 WordPress エクスペリエンスを構築するをご覧ください。

ただし、PHP Wasm はホスト言語の JavaScript のコンテキストでブラウザで実行されます。Chrome では、JavaScript と Wasm は V8 で実行されます。V8 は、ECMA-262 で指定されている ECMAScript を実装する Google のオープンソースの JavaScript エンジンです。また、V8 にはすでにガベージ コレクタがあります。つまり、Wasm にコンパイルされた PHP などを使用するデベロッパーは、ポートされた言語(PHP)のガベージ コレクタ実装を、すでにガベージ コレクタが存在するブラウザに配布することになります。これは、言うまでもなく無駄です。そこで役立つのが WasmGC です。

Wasm モジュールが Wasm のリニアメモリ上に独自の GC を構築できるようにする従来のアプローチのもう 1 つの問題は、Wasm 独自のガベージ コレクタと、Wasm にコンパイルされた言語の上に構築されたガベージ コレクタの間に相互作用がないため、メモリリークや非効率的な収集の試行などの問題が発生する傾向があることです。Wasm モジュールが既存の組み込み GC を再利用できるようにすると、これらの問題を回避できます。

WasmGC を使用したプログラミング言語の新しいランタイムへの移植

WasmGC は、WebAssembly コミュニティ グループ提案です。現在の Wasm MVP 実装では、リニアメモリ内の数値(整数と浮動小数点数)のみを処理できます。参照型の提案がリリースされると、Wasm は外部参照も保持できるようになります。WasmGC に構造体と配列のヒープ型が追加され、非線形メモリ割り当てがサポートされるようになりました。各 WasmGC オブジェクトには固定の型と構造があるため、VM は JavaScript などの動的言語で発生するデオプティマイゼーションのリスクなしで、フィールドにアクセスする効率的なコードを簡単に生成できます。この提案により、構造体と配列のヒープ型を介して、高レベルのマネージド言語を WebAssembly に効率的にサポートします。これにより、Wasm をターゲットとする言語コンパイラをホスト VM のガベージ コレクタと統合できます。簡単に説明すると、WasmGC では、プログラミング言語を Wasm に移植する場合、プログラミング言語のガベージ コレクタを移植する必要はなく、代わりに既存のガベージ コレクタを使用できます。

この改善の実際の影響を検証するために、Chrome の Wasm チームは、CRustJavaFannkuch ベンチマーク(動作時にデータ構造を割り当てる)のバージョンをコンパイルしました。C バイナリと Rust バイナリは、さまざまなコンパイラ フラグに応じて 6.1 K から 9.6 K のいずれかになりますが、Java のバージョンは 2.3 K とはるかに小さくなります。C と Rust にはガベージ コレクタは含まれていませんが、メモリを管理するために malloc/free をバンドルしています。Java がここで小さい理由は、メモリ管理コードをバンドルする必要がまったくないためです。これは 1 つの具体的な例にすぎませんが、WasmGC バイナリが非常に小さくなる可能性を示しています。これは、サイズの最適化に関する大きな作業を開始する前です。

WasmGC に移植されたプログラミング言語の実際の動作

Kotlin Wasm

WasmGC によって Wasm に移植された最初のプログラミング言語の 1 つは、Kotlin/Wasm 形式の Kotlin です。Kotlin チーム提供のソースコードを含むデモを次のリストに示します。

import kotlinx.browser.document
import kotlinx.dom.appendText
import org.w3c.dom.HTMLDivElement

fun main() {
    (document.getElementById("warning") as HTMLDivElement).style.display = "none"
    document.body?.appendText("Hello, ${greet()}!")
}

fun greet() = "world"

上記の Kotlin コードは基本的に Kotlin に変換された JavaScript OM API で構成されているため、何が重要なのか疑問に思うかもしれません。Compose マルチプラットフォームと組み合わせると、デベロッパーは Android Kotlin アプリ用にすでに作成した UI を基にビルドできるため、さらに便利になります。Kotlin チームが提供した Kotlin/Wasm 画像ビューアのデモで、この機能の初期段階をご確認ください。また、ソースコードもご覧ください。

Dart と Flutter

Google の Dart チームと Flutter チームも、WasmGC のサポートを準備しています。Dart から Wasm へのコンパイルはほぼ完了しており、チームは WebAssembly にコンパイルされた Flutter ウェブ アプリケーションを配信するためのツールのサポートに取り組んでいます。作業の現在の状況については、Flutter のドキュメントをご覧ください。次のデモは Flutter WasmGC プレビュー版です。

WasmGC の詳細

このブログ投稿では、WasmGC の概要を説明しました。この機能について詳しくは、以下のリンクをご覧ください。

謝辞

この記事は、Matthias LiedtkeAdam KleinJoshua BellAlon ZakaiJakob KummerowClemens BackesEmanuel ZieglerRachel Andrew がレビューしました。