譯者 | 朱先忠
策劃 | 云昭
WebAssembly已經存在了一段時間,但到目前為止,它對高級語言的實用性有限,尤其是使用垃圾收集的語言。然而,隨著web瀏覽器即將推出對托管內存的支持,情況即將發生變化,這使得WebAssembly成為Scheme、OCaml以及所有非C++或Rust使用者的可行目標。
本文將回顧為什么1.0版本的WebAssembly不是Scheme的好目標,變通方法是什么,新設施是什么,實現將如何利用它們,以及仍然存在哪些限制。在2-3年內,WebAssembly將成為我們許多最熟悉的語言的優秀編譯目標和語言運行時基礎。
WebAssembly,終于要來了。
1、現實中的WebAssembly
WebAssembly可以看作是C編譯器的一個奇怪的后端。目前,只有少數幾種源語言能夠成功運行在WebAssembly上。
Haskell、Ocaml、Scheme、F#等語言呢?我們呢?我們只算是一些懶蟲嗎?
那我們為什么不在那里呢?WebAssembly上的Clojure在哪里?F#、Elixir和Haskell編譯器在哪里?人們早期的確作出一些努力,但并沒有真正成功。為什么?我們只是沒有付出努力嗎?為什么Rust語言可以飛速發展,而Scheme語言卻沒有呢?
WebAssembly的1.0和2.0版本還不能算是良好的支持垃圾收集的語言,讓我們仔細看看其原因。
事實證明,在WebAssembly上尚不存在好的Scheme語言實現是有原因的:如果您的語言依賴于存在垃圾收集器的話,那么WebAssembly的初始版本就是一個可怕的目標。盡管已經取得了一些進展,但對于當前標準化和部署的WebAssembly版本仍然有待觀察。為了更好地理解這個問題,讓我們深入研究這個系統的內部,看看它的局限性是什么。
2、GC和WebAssembly 1.0
基于垃圾收集(GC:Gabage Collecting)的值類型存儲在什么地方呢?
對于WebAssembly 1.0而言,唯一可能的答案是:線性內存。
WebAssembly 1.0為您提供的表示數據的原型是所謂的線性內存:其實,也就是一個可以讀取和寫入的字節緩沖區。除了內存布局更簡單之外,這與本機編譯時得到的非常相似。您可以以64 KB的頁面為單位獲取此內存。在上面的例子中,我們將請求10個頁面,640kB。應該足夠了,對吧?我們將把它全部用于垃圾收集器,并使用一個凹凸指針分配器。堆指針/分配指針保存在可變全局變量$hp中。
這里給出了分配函數的樣子。分配函數$alloc類似于C語言中的malloc函數:它使用一些字節作參數并返回一個指針。在WebAssembly中,指向內存的指針只是一個偏移量,它是一個32位整數(i32)。(使用64位地址空間的選項已經納入計劃中,但還不是標準的實現。)
如果這是您第一次看到WebAssembly函數的文本表示,那么您可以盡情地學習一下,但這卻不是我上面演示內容的重點;我想強調的是call $gc——當分配指針到達區域的末尾時所發生的事情。
3、GC和WebAssembly 1.0(1)
調用$gc背后隱藏著什么?答案是:通過線性內存運送GC。基于“Stop-The-World”技術,采取非并行、非并發機制,而是……根節點。
首先要注意的是,您必須自己提供$gc。當然,這是可行的——這就是我們在編譯到本地目標時所要做的工作。
不幸的是,盡管WebAssembly中的多線程支持有些不足;不過,它允許您共享內存并使用原子操作,只是您必須在WebAssembly之外創建線程。在開發實踐中,您交付的GC可能沒有利用多線程優勢,因此它可能是相當原生態的,以致于將所有垃圾收集工作推遲到“Stop-The-World”階段。
4、GC和WebAssembly 1.0(2)
活動對象包括:
- 根(roots)節點
- 活動對象引用的任何對象
其中,根(roots)節點是活動堆棧幀中的全局值與局部值。
注意:你無法通過編程來訪問活動堆棧幀。
更糟糕的是,您無法訪問堆棧上的根(roots)節點。一個GC必須保留活動對象——它們是被循環定義為根引用的任何對象或活動對象引用的任何物體。從根節點開始,作為全局變量或者是被活動堆棧框架引用的任何GC托管對象。
但是,這種情況下我們遇到了問題,因為在WebAssembly(任何版本,而不僅僅是1.0版本)中,你不能遍歷堆棧,所以你根本找不到活動的堆棧幀;當然,這樣一來你也找不到堆棧根節點。(有時,人們希望將其作為一種低級別的能力來支持;但一般來說,最后形成的共識似乎是,如果由引擎來負責實現GC機制,那么系統的整體性能會更好一些;但目前這僅是一個預兆而已!)
5、GC和WebAssembly 1.0(3)
針對上述問題,一種變通的解決方案是:
- 精確控制堆棧的根節點
- 將所有可能的指針值溢出到線性內存并保守地收集
考慮到堆棧的不可迭代性,基本上有兩種變通的解決方案。一種是讓編譯器和運行時維護對象根節點的顯式堆棧;這樣一來,垃圾收集器可以確定這些根節點是指針。這種辦法很好,因為它可以讓你移動物體。但是,維護堆棧也是一筆開銷;基于現有技術的解決方案是創建一個輔助表(“堆棧映射”),將可以調用GC的每個潛在點與如何找到根節點的指令相關聯。
另一種解決方法是將整個堆棧溢出到內存中。或者,可能只是類似指針的值;無論如何,需要保守地掃描所有關鍵詞,以便搜索到可能是根節點的東西。但是,采用這種方案的話需要我們必須自己訪問內存,而不是訪問WebAssembly實現會溢出堆棧到達的內存區。這種方案可能還湊合;但它算是次優的。有興趣的讀者可以參閱我最近關于Whippet垃圾收集器的帖子(https://wingolog.org/archives/2023/02/07/whippet-towards-a-new-local-maximum),以深入了解基于保守性根節點搜索的含義。
6、GC和WebAssembly 1.0(4)
· 無法收集外部對象(如JavaScript)的循環。
· 指向GC托管對象的指針是對線性內存的偏移,需要在線性內存上具有從外部·讀取/寫入對象的能力。
· 無法將內存回饋給操作系統。
· 想進行詳細的“腸道檢查”:它的回答是“NO”。
如果僅此而已,情況就不那么好了,而且情況會變得更糟!線性內存GC的另一個問題是,它限制了將多個模塊和主機組合在一起的可能性,因為在Web瀏覽器中管理JavaScript對象的垃圾收集器對線性內存上的垃圾收集器一無所知。在這樣的系統中,您可以很容易地創建內存泄漏。
此外,為了讀取或寫入對象的字段,對線性內存中對象的引用需要對所有線性內存進行任意讀寫訪問,這一點非常令人討厭。那么,如何在適當修改的情況下構建一個可靠的系統呢?
最后,一旦你收集了垃圾,也許你設法壓縮了內存,你就不能返回給操作系統任何東西了。對于這點,已經有一些建議正在醞釀中,但還沒有實現。
如果BOB的觀眾必須在“更糟糕的是更好的”和“正確的事情”之間做出選擇的話,我認為BOB的觀眾更接近于選擇“正確的事情”。像這樣的觀眾本能地會對丑陋的系統感到厭惡;我認為GC相對于線性存儲差不多也描述了一個丑陋的系統。
7、GC和WebAssembly 1.0(5)
瀏覽器中已經存在一個高性能并發并行壓縮的GC了。
關鍵是,WebAssembly 1.0要求您編寫和交付一個糟糕的GC,而主機中可能已經有一個很棒的GC——一個投入了數百人年努力的GC,一個肯定會做得比你做得更好的GC。web瀏覽器中托管的WebAssembly應該可以訪問瀏覽器的垃圾收集器!
我有一種感覺,當我們這些對垃圾收集語言情有獨鐘的人,一直站旁觀席上時,Rust和C++程序員們卻一直忙于“在球場上進球”。是的,他們被“球”絆倒了,但最終他們還是設法在擊“球”距離內成功了。
8、變革即將到來!
對內置GC的支持將于2023年第四季度推出。
有了GC,基本條件已經到位。
讓我們將語言編譯到WebAssembly。
為了繼續使用體育環境下“足球”的比喻,我認為在下半場,我們的球員將最終能夠上場,并達到眾所周知的110%。WebAssembly用戶開始支持垃圾收集,我認為即使到今年年底,它也將在主要瀏覽器中推出。這將是一個大事件!我們有機會,我們需要好好把握。
當然,正如我前面所提到的,WebAssembly仍然是一臺奇怪的機器:作為編譯目標有些奇怪,在運行時也是如此。對于調試支持方面,簡直是一場恰到好處的混亂;也許過段時間會有其他關于這方面的文章。
對于如何表示字符串,這是一個令人驚訝的棘手問題;在WebAssembly標準社區中,有人認為JavaScript和WebAssembly可以共享底層字符串表示,也有人認為這是一件愚蠢的事,而認為復制是唯一的出路。我不知道哪一方會獲勝;也許稍后也會有更多關于這方面的內容。
類似地,與JavaScript的整個互操作問題在很大程度上處于早期階段,目前的情況是選擇什么都不做而不是做錯事。您可以將WebAssembly(ref-eq)傳遞給JavaScript,但JavaScript對它無能為力:它沒有原型。現有技術還提供了一個JS運行時,它封裝了每個wasm對象,將wasm模塊中導出的函數代理為對象方法。
最后,一些語言實現確實需要JIT支持,比如PyPy。
9、總結
有了GC支持,WebAssembly現在已經為我們準備好了。
把我們熟悉的語言放在WebAssembly上現在已經是一件很容易的事情了。
那么,讓我們在下半場進球吧!
請訪問以下有關鏈接:
- "gitlab.com/spritely/guile-hoot-updates"
- "wingolog.org"
- "wingo@igalia.com"
- "igalia.com"
- "mastodon.social/@wingo"
事實證明,WebAssembly在C、C++、Rust等方面取得了一些巨大的勝利,但現在輪到我們參與到游戲中了。GC即將到來,作為一個社區,我們需要準備好編譯器和語言運行時。讓我們沖好咖啡,把一些字節放在一起;現在還為時過早,不過,對于擁有最佳WebAssembly體驗的語言社區來說,這是一個值得贏得的世界。
10、WebAssembly是一個令人興奮的新型通用計算平臺
WebAssembly到底是什么?它不是一種你用于編寫軟件的編程語言,而是一種編譯目標:如果你愿意學習的話,它基本算是一種匯編語言。
對于此平臺,它擁有可預測的便攜性能:
- 低層級上運行
- 本地代碼約占不到10%
通過隔離實現可靠的組合:
- 默認情況下,模塊不共享任何內容
- 沒有夢魘般錯誤
- 提供內存沙盒支持
你需要將代碼編譯到WebAssembly,以便更輕松地進行部署和創作。
如果你把WebAssembly的特點看作一臺抽象機器;那么,對我來說,它在以下兩個主要領域比其他機器有所進步。
首先,它接近本質——例如,如果您將圖像處理庫編譯到WebAssembly并運行它,與將其編譯到x86-64或ARMv8或其他版本相比,您將獲得類似的性能。(特別是對于圖像處理,本地運行通常仍然會獲勝,因為WebAssembly中的SIMD(單指令多數據流)原語更窄,而且將圖像放入和取出WebAssembly可能意味著一個要創建一個副本。)WebAssembly的指令集涵蓋了廣泛的低級別操作,這使編譯器能夠生成高效的代碼。
這里的新穎之處在于WebAssembly既可移植,又很成功。我們這些程序員“怪人”知道,僅僅在技術上做得更好是不夠的:你還必須成功地為你的替代方案爭取吸引力。
第二個有趣的特征是,WebAssembly(一般來說)遵循最小權限體系架構:WebAssembly模塊從一開始就只能夠訪問它自己。模塊實例所具有的任何功能都必須在實例化時由主機顯式地與其共享。這不同于可以訪問所有主內存的DLL,也不同于可以改變全局對象的JavaScript庫。這一特性使WebAssembly模塊能夠可靠地組成更大的系統。
11、大肆宣傳WebAssembly吧
所有瀏覽器都支持WebAssembly!因此,您的代碼能夠提供給世界上的任何人使用!
它就運行在你的身邊!從靠近用戶的網站運行代碼!
把一個庫(例如Expat)組合到你的瀏覽器程序(例如Firefox)中,不需要冒任何風險!
這是一種新的輕量級虛擬化:Wasm正相當于容器對虛擬機的作用!因此,不再需要耗費花在Kubernetes上的現金!
同樣,WebAssembly正在取得成功!它在你所有的手機、所有的桌面web瀏覽器、所有的內容分發網絡上;在某些情況下,它似乎會取代云中的容器。
原文鏈接:??https://www.wingolog.org/archives/2023/03/20/a-world-to-win-webassembly-for-the-rest-of-us??
譯者介紹:
朱先忠,51CTO社區編輯,51CTO專家博客、講師,濰坊一所高校計算機教師,自由編程界老兵一枚。