程式設計語言分為兩種:垃圾收集程式設計語言和需要手動記憶體管理的程式設計語言。前者包括 Kotlin、PHP 或 Java 等。後者的例子包括 C、C++ 或 Rust。一般來說,高階程式設計語言中較有可能將垃圾收集當做標準功能。這篇網誌文章將著重於這類垃圾收集程式設計語言,以及如何將這些語言編譯為 WebAssembly (Wasm)。不過,垃圾收集 (通常稱為 GC) 究竟是什麼呢?
瀏覽器支援
垃圾收集
簡單來說,垃圾收集的概念就是嘗試回收程式已分配但不再參照的記憶體。這類記憶體稱為「垃圾」。實作垃圾收集的方法有很多種。其中之一就是參照計數,目的是計算記憶體中物件參照的數量。當物件沒有其他參照時,系統會將其標示為不再使用,並準備進行垃圾收集。PHP 的垃圾收集器會使用參照計數,您可以使用 Xdebug 擴充功能的 xdebug_debug_zval()
函式,一窺其運作原理。請參考下列 PHP 程式。
<?php
$a= (string) rand();
$c = $b = $a;
$b = 42;
unset($c);
$a = null;
?>
程式會將轉換為字串的隨機號碼指派給名為 a
的新變數。接著,它會建立兩個新變數 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 虛擬機器 (VM),而 VM 則由 Zend 編譯器和 Zend 執行工具組成。以 C 等高階語言實作的 PHP 等語言,通常會針對特定架構 (例如 Intel 或 ARM) 進行最佳化,且每個架構都需要不同的後端。在這個情況下,Wasm 代表新的架構。如果 VM 有架構專屬程式碼,例如即時 (JIT) 或預先 (AOT) 編譯,開發人員也應為新架構實作 JIT/AOT 的後端。這個做法非常合理,因為程式碼庫的主要部分通常只需針對每個新架構重新編譯即可。
鑒於低階 Wasm 的運作情況,自然會採取相同的做法:使用剖析器、程式庫支援、垃圾收集和最佳化工具,重新編譯主要 VM 程式碼,並視需要為 Wasm 實作 JIT 或 AOT 後端。自 Wasm MVP 以來,這項功能就已可行,而且在許多情況下都能順利運作。事實上,編譯為 Wasm 的 PHP 是 WordPress Playground 的動力來源。如要進一步瞭解專案,請參閱「使用 WordPress Playground 和 WebAssembly 打造瀏覽器內的 WordPress 體驗」。
不過,PHP Wasm 在瀏覽器中於主機語言 JavaScript 環境中執行。在 Chrome 中,JavaScript 和 Wasm 會在 V8 中執行,這是 Google 的開放原始碼 JavaScript 引擎,可實作 ECMA-262 中指定的 ECMAScript。V8 已具備垃圾收集器。也就是說,如果開發人員使用編譯為 Wasm 的 PHP,最終會將移植語言 (PHP) 的垃圾收集器實作項目傳送至已具備垃圾收集器的瀏覽器,這麼做會造成浪費,這時,WasmGC 就派上用場。
舊版方法讓 Wasm 模組在 Wasm 的線性記憶體上建構自己的 GC,這會導致 Wasm 本身的垃圾收集器與編譯為 Wasm 語言的建構 GC 之間沒有互動,進而導致記憶體外洩和收集效率不佳等問題。讓 Wasm 模組重複使用現有的內建 GC,可避免這些問題。
使用 WasmGC 將程式設計語言移植到新的執行階段
WasmGC 是 WebAssembly 社群群組的提案。目前的 Wasm MVP 實作功能只能處理線性記憶體中的數字,也就是整數和浮點數,而隨著參照類型提案的推出,Wasm 還可以保留外部參照。WasmGC 現在會新增結構體和陣列堆積類型,也就是支援非線性記憶體配置。每個 WasmGC 物件都有固定的類型和結構,因此 VM 可以輕鬆產生有效的程式碼來存取其欄位,不必擔心 JavaScript 等動態語言的不最佳化問題。因此,這項提案透過結構體和陣列堆積類型,為 WebAssembly 新增高階受管理語言的有效支援,讓以 Wasm 為目標的語言編譯器可與主機 VM 中的垃圾收集器整合。簡單來說,這表示使用 WasmGC 將程式設計語言移植至 Wasm 時,程式設計語言的垃圾收集器不再需要納入移植作業,而是可使用現有的垃圾收集器。
為驗證這項改善措施在實際情況下的影響,Chrome 的 Wasm 團隊已編譯 Fannkuch 基準測試 (會在運作時分配資料結構) 的版本,這些版本分別來自 C、Rust 和 Java。C 和 Rust 二進位檔的可能介於 6.1 K 到 9.6 K 之間,視不同的編譯器標記而定,而 Java 版本則小得比 2.3 K 小很多!C 和 Rust 不包含垃圾收集器,但仍會內含 malloc/free
來管理記憶體,而 Java 在這裡較小的原因是因為它根本不需要內含任何記憶體管理程式碼。這只是一個具體範例,但顯示 WasmGC 二進位檔的潛力極小,甚至在針對大小進行最佳化調整之前,先完成這些作業。
查看 WasmGC 移植的程式設計語言實際運作情形
Kotlin Wasm
多虧 WasmGC,Kotlin 是第一個移植至 Wasm 的程式設計語言,以 Kotlin/Wasm 的形式呈現。以下清單顯示 示範,以及 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 後,這項功能就會變得更實用,因為開發人員可以利用已為 Android Kotlin 應用程式建立的 UI 進行建構。透過 Kotlin/Wasm 圖片檢視器示範影片,搶先體驗並探索原始碼,同樣由 Kotlin 團隊提供。
Dart 和 Flutter
Google 的 Dart 和 Flutter 團隊也正在準備支援 WasmGC。Dart-to-Wasm 編譯作業即將完成,團隊也正在努力開發工具支援,以便提供編譯為 WebAssembly 的 Flutter 網頁應用程式。如要瞭解這項工作的目前狀態,請參閱 Flutter 說明文件。以下是 Flutter WasmGC 預覽版的示範。
進一步瞭解 WasmGC
這篇網誌文章並未完全刮傷表面,大部分都是 WasmGC 的概略介紹。如要進一步瞭解這項功能,請參閱下列連結:
特別銘謝
本文由 Matthias Liedtke、Adam Klein、Joshua Bell、Alon Zakai、Jakob Kummerow、Clemens Backes、Emanuel Ziegler 和 Rachel Andrew 共同審查。