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

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

対応ブラウザ

  • 119
  • 119
  • 120
  • x

ガベージ コレクション

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

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

このプログラムは、文字列にキャストされた乱数を a という新しい変数に代入します。次に、bc という 2 つの新しい変数を作成し、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 Compiler と Zend Executor で構成される Zend 仮想マシン(VM)で構成されています。C などの他の高レベル言語で実装される PHP などの言語には、Intel や ARM などの特定のアーキテクチャを対象とした最適化が一般的に行われており、アーキテクチャごとに異なるバックエンドが必要になります。この文脈において、Wasm は新しいアーキテクチャを表します。ジャストインタイム(JIT)コンパイルや事前(AOT)コンパイルなど、VM にアーキテクチャ固有のコードがある場合、デベロッパーは新しいアーキテクチャの JIT/AOT のバックエンドも実装します。多くの場合、新しいアーキテクチャごとにコードベースの主要部分を再コンパイルするだけで済むため、このアプローチは非常に理にかなっています。

Wasm が低レベルであることから、同じアプローチを試すのは自然なことです。つまり、パーサー、ライブラリ サポート、ガベージ コレクション、オプティマイザーを使用してメイン VM コードを Wasm に再コンパイルし、必要に応じて Wasm の JIT または AOT バックエンドを実装します。これは Wasm MVP から可能で、多くのケースで問題なく機能します。実際、Wasm にコンパイルされた PHPWordPress Playground の原動力となっています。プロジェクトについて詳しくは、WordPress Playground と WebAssembly を使用してブラウザでの WordPress エクスペリエンスの構築に関する記事をご覧ください。

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

Wasm モジュールが Wasm の線形メモリ上に独自の GC を構築できるようにするもう一つの問題は、Wasm 独自のガベージ コレクタと Wasm にコンパイルされた言語の組み込みのガベージ コレクタの間に相互作用がないことです。これにより、メモリリークや非効率的な収集の試みなどの問題が発生しやすくなります。Wasm モジュールで既存の組み込み GC を再利用できるようにすることで、このような問題を回避できます。

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

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

この改善による実際の影響を検証するために、Chrome の Wasm チームは、CRustJava のバージョンの Fannkuch ベンチマーク(データ構造が正常に動作すると割り当てられます)をコンパイルしました。C バイナリと Rust バイナリは、さまざまなコンパイラ フラグに応じて 6.1 K から 9.6 K になりますが、Java バージョンはわずか 2.3 K とはるかに小さくなっています。C と Rust にはガベージ コレクタは含まれていませんが、メモリ管理のために malloc/free をバンドルしています。メモリ管理コードをバンドルする必要がないため、Java のサイズが小さくなります。これはほんの一例にすぎませんが、WasmGC バイナリは非常に小さくなる可能性があり、サイズの最適化に大がかりに取り組む前の段階にあることを示しています。

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

Kotlin Wasm

WasmGC のおかげで Wasm に最初に移植されたプログラミング言語の一つは、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/Wasm 画像ビューアのデモで初期段階の探索をご確認ください。また、Kotlin チームのサポートにより、ソースコードを探索できます。

Dart と Flutter

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

WasmGC の詳細

このブログ投稿は、表面をかじっただけのもので、ほとんどが WasmGC の概要をまとめたものです。この機能について詳しくは、以下のリンクをご覧ください。

謝辞

ヒーロー画像(作成者: Gary ChanUnsplash)この記事は、Matthias LiedtkeAdam KleinJoshua BellAlon ZakaiJakob KummerowClemens BackesEmanuel ZieglerRachel Andrew によってレビューされました。