WebAssembly 生態及關鍵技術綜述

小編:本文由 WebInfra 團隊姚忠孝,楊文明,張地編寫。主要以全局視角論述 WebAssembly 技術生態發展情況以及其中涉及到的關鍵技術。是建立完整技術認知和體系的綜述性文章,文章鏈接較多,建議在 PC 上訪問閱讀。

WebAssembly 作爲字節碼和內存模型規範看起來非常簡單且實現起來很有趣,隨着 WebAssembly 規範的演進,新技術不斷湧現,在應用場景中構建自己的生態系統,並不斷髮展成爲一個成熟的平臺。

按上圖所示開發模式,開發者可以通過特定的語言生態開發、發佈和執行應用; 那麼選擇 WebAssembly 的動機是什麼呢?社區有很多這方面的討論,總結可能有如下幾方面。

WebAssembly 作爲一個可安全隔離,高效,體積小、跨平臺,多語言支持的可移植二進制中間形式,可以很好的爲解決如上幾方面的問題提供可行的路徑,不同的開發者可以根據需要利用 WebAssembly 的某一個或多個特性來滿足業務需求。

https://www.youtube.com/watch?v=fh9WXPu0hw8

如上圖所示,對於 JS/Python 等腳本語言來說,爲了追求更高的性能,可以將性能熱點模塊通過 WebAssembly 來實現,從而獲取高性能執行的收益。

對於 Rust 開發者來說,利用語言的特性可以獲取高性能和高安全性,但爲了讓開發者獲得更低的開發門檻,可以編譯爲 WebAssembly 模塊提供給類似 JS , Python 等腳本語言使用,降低開發者門檻;

對於 C++ 開發者來說,可以獲得高性能,但 C++ 不完備的安全性機制可能會使應用存在安全隱患,可以將其編譯爲 WebAssembly 在輕量級安全沙箱中運行,從而使得安全機制做到開箱即用 (安全性保障需要安全領域的專業支持,門檻很高)

WebAssembly 生態包括開發者 (開發語言),編譯工具鏈,利於高性能執行和體積小的 WebAssembly Binary 標準格式,與各種編程語言友好集成機制 ( binding ),以及高性能跨平臺的執行引擎組成。

WebAssembly Languages && ToolChains

https://github.com/appcypher/awesome-wasm-langs

WebAssembly 爲了良好的開發者生態,支持越來越多的語言編譯成  WebAssembly 。語言的支持程度取決於各自編譯器的編譯工具鏈的成熟度,如下部分將梳理相對成熟的幾個可以編譯成 WebAssembly 的語言及其編譯工具鏈情況。

1.1 C/C++

WebAssembly 已經作爲 LLVM 的一個默認支持的後端,通過 Clang 可以將 C/C++ 代碼直接生成 WebAssembly 目標程序 (https://zhuanlan.zhihu.com/p/339950505);

Emscripten 工具鏈主要提供了常用的 C/C++ 庫 (sys-libs),並在 LLVM 的基礎上集成了 Binaryen 的模塊,對 WebAssembly 做進一步優化同時生成 JavaScript API 以便於宿主環境通過 JS 接口集成 WebAssembly 代碼。

sys-libs: https://github.com/emscripten-core/emscripten/tree/main/system/lib

Emscripten: https://github.com/emscripten-core/emscripten

Binaryren: https://github.com/WebAssembly/binaryen

standalone WebAssembly: https://v8.dev/blog/emscripten-standalone-wasm

1.2 Rust

Rust 語言是基於 LLVM 後端實現的編程語言。在編譯器層面來說,Rust 編譯器僅僅是一個編譯器前端,它負責從文本代碼一步步編譯到LLVM中間碼 (LLVM IR),然後再交給LLVM來最終編譯生成機器碼或 WebAssembly 代碼,所以對於 WebAssembly 代碼生成利用的是LLVM編譯後端。

Rust 生態中,Cargo 和 wasm-bindgen 可以自動化編譯過程,生成 WebAssembly 的主語言綁定代碼,大大簡化了 WebAssembly 模塊的生成和使用成本。

1.3 GO

TinyGo 是基於 LLVM 後端實現的 Go 編譯器。在編譯器層面來說,TinyGo 僅僅是一個編譯器前端,它負責從文本代碼一步步編譯到LLVM中間碼 (LLVM IR),然後再交給LLVM來最終編譯生成機器碼或 WebAssembly 代碼 (編譯器結構參照 Rustc)。

Go compiler for small places. Microcontrollers, WebAssembly, and command-line tools. Based on LLVM. https://github.com/tinygo-org

1.4 Java

Java 是基於類的、面向對象的一種通編程語言。Java 以類爲單位將程序編譯爲字節碼在虛擬機上運行,從而實現 “編寫一次,隨處運行”(WORA)。

https://github.com/i-net-software/JWebAssembly

1.5 Javascript/TypeScript/AssemblyScript

1.5.1 Assemblyscript

https://github.com/AssemblyScript/assemblyscript/blob/main/src/README.md

https://github.com/WebAssembly/binaryen

AssemblyScript 是面向 WebAssembly 標準的嚴格類型化的 TypeScript 變體,通過 Binaryen 後端編譯爲 WebAssembly 。如下圖所示,TypeScript 和對應的 lib 庫通過編譯器前端生成 AssemblyScrip t,並集成 Glue 代碼片段利用 Binaryen 後端生成 WebAssebmly Module。

(https://www.assemblyscript.org/introduction.html#from-a-javascript-perspective)

1.5.2 Assemblyscript  <XScript < TypeScript(JavaScript)

XScript 用於代表一類介於 Assemblyscript 和 TypeScript 之間的語言集,其語法和易用性類似於 TypeScript ,但它屬於靜態,強類型語言,可以利用通過靜態編譯來提升性能。

StaticScript 是一門 TypeScript 語法兼容的靜態編譯型語言,其基本設計思路是通過 " 受限類型系統來約束和指導靜態編譯 (https://staticscript.org/guide/type),其編譯流程如下圖所示:

StaticScript 設計目的和 AssemblyScript 類似 (靜態編譯),但採用的路徑有所差別,AssemblyScript 類似於原始 WebAssembly 的一層 thin layer 語言層封裝;而 StaticScript 是從類型系統着手支持靜態編譯。(完備的類型系統需要考慮語言的圖靈完備性,類型系統的 soundness 等衆多方面,具有一定的難度和複雜度)

ts2wasm

除了自定義語言外,另外一個方案是基於 TypeScript 語言,擴展和定製編譯工具鏈以編譯生成 WebAssembly 代碼。

1.5.3 JavaScript

JavaScript 是一種符合 ECMAScript 規範的動態、弱類型、基於原型和多範式的語言,其性能,安全隔離性等方面受到廣泛的關注。由於 JavaScript 的弱類型和動態性,在降低開發者門檻的同時也爲靜態分析,靜態編譯,性能優化帶了不小的阻礙;爲此,社區進行了廣泛而深入的研究,我們嘗試從如下基本來做一個初步的梳理和討論。

(1) 性能敏感模塊 WebAssembly 化

爲了提升基於 JavaScript 應用性能,V8 等虛擬機引入了 JIT 機制。JIT Pipeline (如下所示) 首先需要將 JavaScript 編譯爲後端代碼,在應用啓動性能,內存佔用上都會存在損耗,而爲了快速啓動的解釋執行相比較 WebAssembly 來說也存在一定的性能差異,因此,對於 CPU 密集型計算(例如處理數學,圖像處理) 等任務可以利用 WebAssembly 來進行優化;此外 wasm 可以大大減少移動設備上的電池消耗(取決於引擎),因爲大部分處理步驟在編譯期間已經提前完成 (基於 JavaScript 字節碼發佈或者緩存可以達到類似效果,但 JavaScript 字節碼沒有統一的規範和標準,無法作爲通用產物分發)。

https://blog.sessionstack.com/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it-d80945172d79

(2) Javascript AOT Engine

JIT 可以極大的提升 JavaScript 性能,禁用 JIT 的 V8 可能比啓用 JIT 的 V8 慢 x5~x17 倍,因此 JIT 對於實現可接受的幀率至關重要。而對於受限硬件,特殊系統平臺 (iOS) 大多數控制檯不允許用戶應用程序在運行時創建可執行代碼,從而有效地消除了使用 JIT 的可能性。

在無法使用 JIT 的情況下,要運行 JavaScript 有如下 3 個可能的方式:

以上三種方案中,解釋執行的模型在性能上無法滿足需求;而用新語言重寫項目無論是通用性和規模效應都是無法接受的。那麼唯一的選項就是 AOT (靜態編譯)。而 JavaScript 是弱類型的動態語言,傳統的靜態編譯需要明確獲取類型信息,似乎無法實現 AOT ,那麼提升 JavaScript 的 "銀彈" 是什麼呢?

"靜態編譯的基本原則是封閉性假設 (closed world assumption),要求編譯器在編譯時必須掌握運行時所需的全部信息,換句話說,就是運行時不能出現任何編譯時未知的內容"。從以上的定義其實我們可以看出,並沒有嚴格定義動態類型不能進行 AOT ,它要求的是需要在編譯期做到 "封閉性",也就是說所有的編譯期需要分析和處理所有可能性,在運行期間出現的內容必須是編譯期內容的 "子集 (或真子集)"。

JavaScript AOT compilation: https://hal.archives-ouvertes.fr/hal-01937197/document

如果換一個角度來講,JIT 機制利用 profile 的數據將部分熱點函數通過編譯過程轉換爲高效的機器碼進而提升性能,而 AOT 則通過一定的機制和假設 (speculative optimization) 將 JIT 過程提前到編譯期完成。

下面主要基於如下幾個假設簡單闡述 AOT 的相關流程:

假設一:

大多數 JavaScript 程序在啓動階段後表現出相當靜態的行爲 (這也是 JIT 的基礎)。一旦應用加載完成並完成 "預熱後"(初始化),對象的原型鏈很少會發生變化 (數據結構,繼承關係,大多數全局變量結構也很少會改變。

在 AOT 過程中,靜態編譯器可以充分利用這一假設,ChowJS 大致過程如下圖所示。

首先,ChowJS 解釋器加載 JavaScript 運行,在應用啓動階段完成後,對應的方法均已編譯爲字節碼。

其次,對每個編譯爲字節碼的函數 (F)

對於 AOT 編譯,我們必須能夠訪問啓動 JS 的上下文,因爲這使得用戶對象、原型和方法在編譯時可供我們使用,並使優化更加直接。詳細過程訪問如下鏈接:

以上重點討論了 JavaScript 靜態編譯的可行性及流程,能夠靜態編譯爲 C 代碼,那麼就能夠順利的靜態編譯爲 WebAssembly 。因此,對於可 AOT 的部分,生成 WebAssembly 模塊也是一個可選項。

假設二:

即使在看到實際 JS 代碼之前,我們就可以預見到需要生成一系列的 IC stubs ,因爲 JS 中會經常使用到一些固定的模式編程模式。一個很好的例子是訪問對象的屬性。這在 JS 代碼中經常發生,並且可以通過使用 IC Stub 加速訪問。對於具有特定 “形狀” 或“隱藏類”(即以相同方式佈置的屬性)的對象,當從這些對象獲取特定屬性時,該屬性將始終處於相同的偏移量。雖然在 JS 運行前無法獲取實際的類型信息,但可以 “parameterize IC stub” 及生成屬性訪問的函數模板,並且將類型信息和屬性偏移作爲參數傳遞給 IC stub 。

經測試發現,僅需幾 KB 的 IC stubs 就可以覆蓋絕大多數 JS 代碼。例如,使用 2 KB 的 IC stubs ,我們可以覆蓋 Google Octane 基準測試中 95% 的 JS 代碼,詳細內容見下圖及 "Fast AOT-compiled JS" 鏈接所示。

Fast AOT-compiled JS: https://bytecodealliance.org/articles/making-javascript-run-fast-on-webassembly

如前所述,JavaScript 是動態弱類型語言,我們可以通過一定的機制對熱點做 AOT ,但純靜態編譯需要遵循 "封閉性" 原則,對於靜態特性之外的部分,需要通過 Fallback 機制進行兼容,從而滿足 "封閉性假設"(deopt);Fallback 機制根據不同的設計有多種不同的實現,例如基於 AST 的解釋執行,基於 Bytecode 的解釋執行,基於未優化的機器碼執行等方式。這意味着實際引擎會有幾個執行層:

SideNote:

(3) WebAssembly 沙箱運行 JavaScript

WebAssembly 的輕量部署,高效冷啓動和進程級安全沙箱特性,對於應用快速啓動和安全隔離具有重要的作用。如果該語言無法編譯爲 WebAssembly ,它將如何在 wasm-runtime 上運行?其中一個可行的方案是將語言的運行時編譯爲 WebAssembly ,而不是將程序本身編譯爲 WebAssembly 。

但這個又帶來了另外一個性能問題,因爲 WebAssembly 不能使用 JIT ,不允許動態生成新的機器代碼並在純 Wasm 代碼中運行,而只能使用解釋器。

經調查發現,JS 運行過程可以大致分爲初始化階段和執行階段,而初始化階段可以在 AOT 過程運行,並將初始化後的虛擬機狀態保存爲 snapshot 數據。當實際執行應用時,可以加載 snapshot 而跳過初始化環節,從而大大提升應用啓動性能 (詳細內容請訪問如下鏈接內容)。

Making JavaScript run fast on WebAssembly: https://bytecodealliance.org/articles/making-javascript-run-fast-on-webassembly?_fsi=UYShxlrT

Hit the Ground Running: Wasm Snapshots for Fast Startup: https://www.youtube.com/watch?v=C6pcWRpHWG0&t=1338s

此外,我們可以採用 JavaScript 的 AOT 機制來優化 JavaScript 代碼,以提升 wasm 引擎的吞吐量。

WebAssembly Runtimes

WebAssembly 是輕量,跨平臺的標準二進制指令格式,基於 WebAssembly 可以實現 “編寫一次,隨處運行”(WORA)。隨着 WebAssembly 不斷髮展,並不斷髮展成爲一個成熟的平臺。實現 WebAssembly 運行時變得更具挑戰性和耗時,各 WebAssembly 運行環境逐漸選擇構建自己的生態系統,如 wastime 、Wasmer、V8、Lucet、WAMR 等。

更多內容見如下鏈接: https://github.com/appcypher/awesome-wasm-runtimes

2.1 wasmtime(Bytecode Alliance project with Rust)

https://github.com/bytecodealliance/wasmtime

Wasmtime 是一個字節碼聯盟項目,用於在非 Web 環境執行 WebAssembly 和 WASI 平臺綁定。

Wasmtime 是由非盈利組織 Bytecode Alliance 主導,其成員是 WebAssembly 的主要推動者(核心成員來自 mozilla Rust+Wasm 團隊,核心代表任務是 lin clark 及其團隊成員),因此,在 Wasm Spec 的演進,新特性實現,Wasm 工具鏈,Wasm 生態建設方面,wasmtime 將是最佳的試驗場。

https://bytecodealliance.org/articles/1-year-update

Note: The Bytecode Alliance doesn’t host specifications. While BA members are driving specs mentioned below, they are doing that in collaboration with others in the W3C WebAssembly CG. Bytecode Alliance projects include implementations of these specs.

Wasmtime 支持了 WebAssembly 的標準功能,但當前強依賴自研的 Cranelift 編譯後端而不支持 LLVM ,所以目前僅在 x86_64 上保證可用並且性能比 llvm 要差很多 (詳見性能對比),另外,對其他平臺的支持還在進行。“The x86-64 backend is currently the most complete and stable; other architectures are in various stages of development.”

wasmtime 包括如下核心模塊

詳細內容參見: Architecture of Wasmtime: https://docs.wasmtime.dev/contributing-architecture.html#architecture-of-wasmtime

2.2 wasmer (with Rust)

https://wasmer.io/

wasmer 由 Wasmer Inc. 開發並開源的高性能且安全的 WebAssembly 運行時,提供能夠在從桌面到雲、邊緣和物聯網設備等隨處可運行的超輕量級容器。

wasmer 面向商用場景着力打造應用生態,用戶體驗,極致性能,提供了通用、完善的功能,豐富的文檔、社區,論壇、博客,廣泛的語言和工具 (wapm) 支持,其最大的好處是 "開箱即用"。

https://docs.wasmer.io/ecosystem/wasmer/wasmer-features

https://docs.rs/wasmer/2.0.0/wasmer/

正如 wasmer 所宣傳的

"Wasmtime did the basics. Wasmer added your language and skyrocketed speed."

https://wasmer.io/wasmer-vs-wasmtime

2.3 WasmEdge(A cloud native WebAssembly runtime with C/C++)

https://github.com/WasmEdge/WasmEdge

WasmEdge 的前身是 SSVM (SecondState VM) ,主要應用於 "應該使用 Docker 而 Docker 用不起來的地方",即把雲原生這套理念和工具應用到邊緣計算場景。

A Cloud-native WebAssembly Runtime: https://www.youtube.com/watch?v=9LpvgWaG_T0

WasmEdge 成爲 CNCF Sandbox 項目: https://toutiao.io/posts/m7p2v10/preview

如上圖所示,WasmEdge 最大的特點是將執行引擎融入了 k8s(Kubernetes) 的雲原生框架中,爲雲原生的執行增加了高性能,輕量的 WebAssembly 執行引擎 (基於 LLVM AOT 優化)。

2.4 wamr(Bytecode Alliance project with C/C++)

WebAssembly Micro Runtime (WAMR) 是一個輕量級的獨立 WebAssembly 執行引擎,非常適合資源極其有限的小型嵌入式設備 (佔用空間小,使用解釋器來保持較低的內存開銷具有佔用空間小)、此外,也可用於從嵌入式、物聯網、邊緣到可信執行環境 (TEE)、智能合約、雲原生等的應用程序。

WAMR 項目包括以下三部分功能:

https://github.com/bytecodealliance/wasm-micro-runtime

https://www.w3.org/2020/08/29-chinese-web/wamr.pdf

https://gw.alipayobjects.com/os/bmw-prod/dfadad7f-b3f0-48e7-b7b0-14a4fad65efc.pdf

2.5 V8(with C/C++)

V8 是 Google 開發並開源的高性能 JavaScript 引擎,同時也能夠運行 WASM 模塊。爲了保證 WebAssembly 一個可預期的啓動和執行性能,V8 專門爲 WebAssembly 加入了一個全新的 WebAssembly baseline 編譯器 - Liftoff 。Liftoff 的目標是通過儘可能快地生成代碼來減少基於 WebAssembly 的應用程序的啓動時間。代碼質量是次要的,因爲最終使用 TurboFan 重新編譯熱代碼。Liftoff 避免了構建 IR 的時間和內存開銷通過 WebAssembly 函數的字節碼一次性生成機器代碼。

有了 Liftoff ,現在 V8 引擎針對 WebAssembly 有了兩個編譯層級:Liftoff 作爲 baseline 編譯器提供快速啓動的能力,TurboFan 作爲優化編譯器提供最佳性能。這就帶來了一個問題,如何協調使用這兩個編譯器以帶來全局最佳的用戶體驗。V8 選擇了 “eager tier-up” 策略,在完成模塊的 Liftoff 編譯之後,WebAssembly 引擎立即啓動後臺線程以生成模塊的優化代碼。這允許 V8 快速開始執行代碼(在 Liftoff 完成後),但仍然儘可能提供性能最高的 TurboFan 代碼。

但 V8 是專爲 JavaScript 優化的,無法獨立 WebAssembly standalone Runtime ,而和 JavaScript 引擎融合在一起,導致體積過大,並且在 WebAssembly 的執行過程中冷啓動時間較長。此外,V8 的隔離是通過使用 Isolate 實現的,而在雲原生場景下,V8 無法提供進程級安全隔離性。

對於執行引擎,性能是最重要的衡量指標之一。Libsodium 長期以來一直以 WebAssembly 爲目標,其內置 benchmark 測試集合可以在各種獨立的 WebAssembly runtime 上運行。

從如下的測試結果可以看出:

https://github.com/jedisct1/webassembly-benchmarks/tree/master/2021-Q1

對於開源項目的選型並非是非此即彼的選擇題,特別是 wasmtime 和 wasmer 都是基於 Rust ,有很多的共同基礎,如果能夠基於兩個引擎的優勢做一定程度的相互融合,一方面可以利用兩個社區的力量,同時也可以根據實際的業務場景進行有效整合,也許是一個值得嘗試的引擎演進路線。

wasmer 的通用、完善的功能,豐富的文檔、社區,論壇、博客,廣泛的語言和工具支持,對於對實際的業務提供更好的支持,可以作爲業務側生態建設的參考標準。

wasmtime 緊跟標準演進,可以提供最新的特性和能力,此外,從上述的性能分析可以看出基於 LLVM backend 的編譯器可以獲取較高的性能,LLVM backend 是一個全球開發者共建的高性能代碼生成引擎,cranelift 之外,wasmtime 借鑑 wasmer 採用 LLVM backend 作爲代碼生成器也許是一個可以考慮的融合方式。

WasmEdge 是一個基於 LLVM 的運行時,專注於性能,仍然是一個年輕的項目,目前僅支持 Linux/x86_64,可能還不是很穩定,可以持續關注。

Relation to wasmtime: https://github.com/wasmerio/wasmer/issues/142

ActuallyUsingWasm: https://wiki.alopex.li/ActuallyUsingWasm

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ZZbRRPTYAu8qUmLwKCs6ew