プログラミング言語には、ガベージ コレクションを行うプログラミング言語と、手動でのメモリ管理が必要なプログラミング言語の 2 種類があります。前者の例としては、Kotlin、PHP、Java などがあります。後者の例としては、C、C++、Rust などがあります。一般的に、高水準プログラミング言語では、ガベージ コレクションが標準機能として搭載されている可能性が高くなります。このブログ投稿では、このようなガベージ コレクション プログラミング言語と、それらを WebAssembly(Wasm)にコンパイルする方法に焦点を当てます。そもそもガベージ コレクション(GC とも呼ばれます)とは何でしょうか?
Browser Support
ガベージ コレクション
簡単に言うと、ガベージ コレクションの考え方は、プログラムによって割り当てられたものの、参照されなくなったメモリを再利用しようとする試みです。このようなメモリはガベージと呼ばれます。ガベージ コレクションを実装するための戦略は数多くあります。その 1 つが参照カウントです。これは、メモリ内のオブジェクトへの参照の数をカウントすることを目的としています。オブジェクトへの参照がなくなると、そのオブジェクトは使用されなくなったものとしてマークされ、ガベージ コレクションの準備が整います。PHP のガベージ コレクタは参照カウントを使用します。Xdebug 拡張機能の xdebug_debug_zval()
関数を使用すると、その内部を覗き見ることができます。次の PHP プログラムを考えてみましょう。
<?php
$a= (string) rand();
$c = $b = $a;
$b = 42;
unset($c);
$a = null;
?>
プログラムは、文字列にキャストされた乱数を a
という新しい変数に代入します。次に、2 つの新しい変数 b
と c
を作成し、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 以降可能であり、多くの場合、実際にうまく機能します。実際、Wasm にコンパイルされた PHP は WordPress Playground の基盤となっています。このプロジェクトについて詳しくは、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 Community Group の提案です。現在の Wasm MVP 実装では、線形メモリ内の数値(整数と浮動小数点数)のみを処理できます。また、参照型の提案がリリースされたことで、Wasm は外部参照も保持できるようになりました。WasmGC に構造体と配列のヒープ型が追加され、非線形メモリ割り当てがサポートされるようになりました。各 WasmGC オブジェクトには固定の型と構造があるため、VM は JavaScript などの動的言語で発生する 最適化解除のリスクを回避しながら、フィールドにアクセスするための効率的なコードを簡単に生成できます。この提案により、Wasm をターゲットとする言語コンパイラがホスト VM のガベージ コレクタと統合できるようになる構造体と配列のヒープ型を介して、高レベルのマネージド言語の効率的なサポートが WebAssembly に追加されます。簡単に言うと、WasmGC を使用すると、プログラミング言語を Wasm に移植する際に、プログラミング言語のガベージ コレクタを移植の一部にする必要がなくなり、代わりに既存のガベージ コレクタを使用できるようになります。
この改善による実際の影響を確認するため、Chrome の Wasm チームは C、Rust、Java から Fannkuch ベンチマーク(動作中にデータ構造を割り当てる)のバージョンをコンパイルしました。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 Multiplatform と組み合わせると、より意味のあるものになります。Compose Multiplatform を使用すると、デベロッパーは Android Kotlin アプリ用にすでに作成した UI を基盤として構築できます。Kotlin チームが提供する Kotlin/Wasm 画像ビューアで、この早期の探索をご覧ください。
Dart と Flutter
Google の Dart チームと Flutter チームも、WasmGC のサポートを準備しています。Dart から Wasm へのコンパイル作業はほぼ完了しており、チームは WebAssembly にコンパイルされた Flutter ウェブ アプリケーションを配信するためのツールサポートに取り組んでいます。現在の作業状況については、Flutter のドキュメントをご覧ください。次のデモは Flutter WasmGC プレビューです。
WasmGC の詳細
このブログ投稿では、WasmGC の概要を大まかに説明したにすぎません。この機能について詳しくは、以下のリンクをご覧ください。
謝辞
この記事は、Matthias Liedtke、Adam Klein、Joshua Bell、Alon Zakai、Jakob Kummerow、Clemens Backes、Emanuel Ziegler、Rachel Andrew によってレビューされました。