當前位置: 妍妍網 > 資訊

生來取代Docker、JS,谷歌力推,這項技術釋出7年後,現狀如何?

2024-03-29資訊

「如果2008年的時候,WASM 和 WASI(WebAssembly System Interface, WASM 系統介面)這兩個東西已經存在了的話,我們就沒有必要創立 Docker 了。 在伺服器上執行 WebAssembly 是計算的未來,目前缺少的就是一個標準的系統介面,希望 W A SI 能夠彌補上這塊缺失的拼圖。 」——Docker 創始人 Solomon Hykes

2017 年釋出的 WebAss embly 技術曾是關註的焦點,釋出之初即被寄予厚望,被視為 JS 的替代者,8-15倍的效能提升讓人興奮不已。 而隨著 Docker 創始人在 2019 年 3 月釋出的一條 Twitter 又讓人暢想起了其在伺服端的套用。 Java 所提出的「一次編譯,多處執行」似乎將進一步實作。 時間來到 2024 年,WebAssembly 從網紅跌落神壇,技術推廣並不成功,90% 以上的場景不需要 WebAssembly,但其技術發展卻在持續成熟。 脫離技術炒作的喧囂,本文作者從WebAssembly 的前世今生說起,深入介紹了這門面臨七年之癢的潮流技術的發展歷程與現狀。

作者 | 段魚

出品丨騰訊雲開發者

WebAssembly,前身技術來自 Mozilla 和 Google Native Client 的 asm.js, 首次釋出於 2017 年 3 月。並於 2019 年 12 月 5 日正式成為 W3C recommendation,至此成為與 HTML、CSS 以及 JavaScript 並列的 Web 領域第四類程式語言。

在 Web 領域,已經有 JavaScript 這樣的利器,而 WebAssembly 則是開啟新世界的大門。WebAssembly 並 不是要取代 JavaScript,而是要在圖形影像處理、3D 遊戲、AR/VR 這些套用領域開疆拓土。如今的現代瀏覽器已經越發朝著微型」第二作業系統「發展,人們希望在瀏覽器內能完成更多的事情,而 WebAssembly 作為 Web 端高效能套用的基石,正在讓更多的套用場景在瀏覽器內變為現實。

除了在瀏覽器內實作高效能套用,WebAssembly 也可以脫離 Web 端在搭載了不同硬體和作業系統的各個平台執行,進一步實作當年 Java 所期望的「一次編譯,多處執行」。 WebAssembly 在伺服端可用於微服務平台、無服務平台、 第三方外掛程式 系統等場景。

WebAssembly 的前世今生:從 Mozilla 說起

1.1 一家偉大的互聯網企業

說起 WebAssembly,那就必須從一家沒落而又偉大的互聯網公司說起,它就是火狐瀏覽器的開發者 Mozilla。 Mozilla 的前身是大名鼎鼎的網景公司(Netscape),也就是 JavaScript 的開發者。 從做瀏覽器起家一路坎坷至今,Mozilla 最近更是頻頻傳出裁員風波,其根源依然是沒有找到太好的盈利點。 作為互聯網開源社群的領跑者,Mozilla 在技術上的成就遠高於其在商業領域。 除了 JavaScript 和 Filefox,Mozilla 還留下了 Rust 、HTML5、MDN(Mozilla Developer Network)以及 asm.js 這些引領互聯網行業發展的重要基石。

1.2 腦洞大開的想法:瀏覽器裏跑 C++

2012年 Mozilla 的工程師在研究 LLVM 時,突然腦洞大開提出了一個想法: 類似遊戲引擎這樣的高效能套用大多都是 C/C++語言寫的,如果能將 C/C++轉換成 JavaScript ,那豈不是就能在瀏覽器裏跑起來了嗎? 如果可以實作,那麽瀏覽器是不是也就可以直接跑 3D 遊戲之類的 C/C++套用? 於是 Mozilla 成立了一個叫做 Emscripten 的編譯器研發計畫,Emscripten 可以將 C/C++程式碼編譯成 JavaScript,但不是普通的 JS,而是一種被特殊改造的 JS,其被命名為 asm.js。

Emscripten 的官方描述是:

Emscripten is a toolchain for compiling to asm.js and WebAssembly, built using LLVM, that lets you run C and C++ on the web at near-native speed without plugins.

中文譯文:

Emscripten 是一個基於 LLVM 的將 C/C++編譯到 asm.js 和 WebAssembly 的工具鏈,它可以讓你在 Web 上以接近原生的速度執行 C/C++而不需要任何外掛程式。

如下圖所示: 實際上,不只是 C/C++程式碼,只要能轉換成 LLVM IR 的語言,都可以透過 Emscripten 轉換成 asm.js。

1.3 另一次失敗的嘗試:Google Native Client

Google 在很早之前也一直致力於研究如何讓 C/C++能夠在 Chrome 裏執行起來,並在2009年的安全領域頂級會議 IEEE Symposium on Security and Privacy 發表了 Google 的技術方案 NaCl(Google Native Client)以及 PNaCl(Portable Google Native Client)。 NaCl 的本質也是一種沙盒技術,使用工具鏈編譯後的 C/C++程式碼能夠以接近原生套用的速度在 web 端執行,也可以與 JS 和 webapi 進行互動。 NaCl 在安全這塊做了大量的設計,其使用了內外雙層沙盒,並利用 x86 記憶體分段機制來隔離記憶體,甚至還用上了靜態代分碼析技術來做沙盒裏執行的程式進行檢查。

然而在經過了8年的掙紮後,在2017 年5月30日 Google 宣布棄用 NaCl。 其根 本原因是 NaCl 這套方案只有自家的 Chrome 願意配合支持,所以壓根就不具備跨瀏覽器執行的能力。最終 Chrome 與 Mozilla 達成一致,共同推進 WebAssembly 方案,Chrome 也直接用 WebAssembly 替換掉了 NaCl。

asm.js:WebAssembly 的前身,一種更快的 JS

2.1 C++轉換 asm.js 範例

一般來說,asm.js 並不是直接編寫的,而是一個面向 JS 編譯器的中間產物。 例如以下的 C++ 程式碼:

//計算i+1intf(int i){return i + 1;}//計算字串長度size_tstrlen(char *ptr) {char *curr = ptr;while (*curr != 0) { curr++; }return (curr - ptr);}

使用 Emscripten 轉換後,生成的 JS 程式碼如下:

functionf(i) { i = i|0;return (i + 1)|0;}functionstrlen(ptr) { ptr = ptr|0;var curr = 0; curr = ptr;while ((MEM8[curr>>0]|0) != 0) { curr = (curr + 1)|0; }return (curr - ptr)|0;}

可以看到這種生成的 JS 跟普通 JS 還是區別很大的,就像剛才我們所說, 程式設計師不直接編寫 asm.js 程式碼,這些看起來怪異的語法都是為了配合編譯器生成更高效的機器碼 。比如在 asm.js 裏反復出現的"按位或"操作,其目的是將原來 JavaScript 裏的 double 型別計算轉為整形運算(CPU進行整形運算的速度快於浮點型)。而這裏被命名為 MEM8 的陣列實際上充當了"堆"的作用。如果只是作為使用者可以不用深究這些最佳化的具體實作,直接使用 Emscripten 來幫助我們完成這一轉換過程即可。

2.2 asm.js 為什麽比原生 JavaScript 快?

由於 asm.js 在瀏覽器中執行,其效能在很大程度上也取決於瀏覽器和 JS 引擎的最佳化支持。2015年6月,Microsoft Edge 也開始加入了對 asm. js 的支持。為了直觀展示 asm.js 所帶來的的效能提升,微軟釋出了一個叫做"Chess Battle"的 demo。Chess Battle

讓兩個版本的開源象棋 AI 對戰,其中一個用 C 實作然後轉成 asm.js,另外一個用原生JS實作。如下圖所示,每個走棋回合限制為200毫秒,其中 asm.js 版本的 AI 因為可以在每個回合進行更多的評估運算(用於決定走棋策略),勝率獲得了極大提升。

asm.js 執行的快慢取決於不同的測試用例、執行硬體、瀏覽器引擎最佳化程度等,一般來說我們可認為 asm.js 能達到原生 C/C++執行速度的50%, 有些場景下甚至能持平 Clang 編譯的 C/C++用例。asm.js 執行比原生 js 快,那麽它如此高效的原因是什麽呢?阮一峰在他的一篇部落格裏寫到的結論是:

一旦 JavaScript 引擎發現執行的是 asm.js,就知道這是經過最佳化的程式碼,可以跳過語法分析這一步,直接轉成組合語言。另外,瀏覽器還會呼叫 WebGL 透過 GPU 執行 asm.js,即 asm.js 的執行引擎與普通的 JavaScript 指令碼不同。這些都是 asm.js 執行較快的原因。

這篇部落格應該是對很多人造成了誤導,具體錯誤在於:

  • 首先,"跳過語法分析,直接生成組譯"是不存在的,語法分析是編譯中不可缺少的一環節,asm.js 跟原生 JS 的編譯執行過程是一致的。

  • 其次,WebGL 作為一個圖形 api 和 asm.js 技術可以說是沒有任何直接關系,原生JS也呼叫 WebGL 來實作 GPU 硬體加速。

  • 最後,也是最離譜的一點,WebGL 透過 GPU 執行 asm.js ?不管是 asm.js、原生 JavaScript 還是 WebAssembly 其編譯產物都是 CPU 機器碼而不是 GPU 機器碼。而且 WebGL 只是一個圖形渲染 api,就算是把J S 編譯到 GPU 也需要類似 CUDA/OpenCL 這些通用計算 api 來支持。最新的 WebGPU 同時支持了圖形和通用計算,這倒是目前 Web 端在 GPU 裏"執行 JS"的可行方法。

  • 先拋開 JavaScript 不談,我們可以思考一下,對於任何一門程式語言來說決定其執行快慢的根源是什麽呢?我認為用一句話來總結就是: 程式碼執行的快慢,從硬體層面上看,直接取決於生成的機器碼所需時鐘周期的總和。從程式語言層面上看,取決於編譯後的產物在執行時有多少"動態決議"。

    例如,弱型別語言比強型別語言慢,是因為編譯時型別是不確定的,需要執行時進行額外的型別推導,這就是"動態決議";

    例如,C++ 裏虛擬函式比普通函式開銷大,是因為編譯時函式地址是不確定的。普通函式編譯後生成的跳轉目的地是一串固定的地址,而虛擬函式的跳轉地址是在執行時從 CPU 的寄存器裏讀取的,這也是"動態決議",編譯後的機器碼多了一條寄存器取值指令;

    類似的場景還有 GC 機制、樣版編程、JIT 最佳化等等,歸根結底就是如果在編譯時候能完成更多事情,那麽生成的機器碼執行周期就越短,程式碼也就執行地越快。asm.js 在減少執行時的"動態決議」這裏所做的工作,wiki 原文如下:

    Much of this performance gain over normal JavaScript is due to 100% type consistency and virtually no garbage collection.

    可轉譯為:

    與原生 JavaScript 相比,這裏效能提升的主要原因是100%的型別一致性以及幾乎沒有(自動的)垃圾回收機制。

    簡而言之就是,asm.js 的實作去掉大部份的自動 GC 機制,然後改成了強型別語言,編譯器能夠更大程度地進行最佳化,這才是 asm.js 能比普通 JS 執行更快的原因。 在 asm.js 裏不再支持除了浮點和整形之外的型別,記憶體的開辟和釋放也需要程式碼手動進行處理。部份引擎甚至還可以以 AOT 或者 JIT 的形式執行 asm.js。關於 asm.js 的原理,在微軟的文件裏也有一段更加詳細的描述:

    Asm.js is a strict subset of JavaScript that can be used as a low-level, efficient target language for compilers. As a sublanguage, asm.js effectively describes a sandboxed virtual machine for memory-unsafe languages like C or C++. A combination of static and dynamic validation allows JavaScript engines to employ techniques like type specialized compilation without bailouts and ahead-of-time (AOT) compilation for valid asm.js code. Such compilation techniques help JavaScript execute at 「predictable」 and 「near-native」 performance, both of which are non-trivial in the world of compiler optimizations for dynamic languages like JavaScript.

    這段話從編譯器最佳化的角度對 asm.js 原理描述地非常貼切了,比較難準確轉譯,大概釋義如下:

    asm.js 是 JavaScript 的一個嚴格子集,是一種面向編譯器的底層且高效的目標語言。作為一種子語言,asm.js 高效地為類似 C/C++這樣的記憶體不安全語言描述了一個沙盒虛擬機器。靜態驗證和動態驗證的結合允許 JavaScript 引擎對有效的 asm.js 程式碼使用型別特化編譯和提前(AOT)編譯等技術。這樣的編譯技術可以幫助 JavaScript 具有"可預見性"和「接近原生」的效能表現,這兩種特性在 JavaScript 這樣的動態語言編譯器最佳化中是非常重要的。

    其中"bailouts"應該是微軟這個 JS 編譯器裏的專用名詞,沒有特別合適的轉譯。"predictable"可理解為「更少的動態決議」。asm.js 目前看已經是過時的技 術,並非本文的重點也不再展開繼續討論,如果想繼續了解 JavaScript 編譯最佳化的實作細節,讀者可參閱文獻的內容自行研讀。

    WebAssembly:繞過 JS 直接生成機器碼

    Asm.js 的思路是將一種程式語言轉換成另外一種程式語言,輸出的還是 JS 程式碼。 那麽這裏你肯定也想到了,我們為什麽不能繞過 JavaScript ,將 C/C++程式碼直接轉成瀏覽器可以辨識的更底層的語言呢? 這就是由 Asm.js 衍生出的WebAssembly 技術。

    3.1 WebAssembly 是什麽?

    如上圖所示,為了能便於程式設計師閱讀和編輯 WebAssembly,源碼除了被編譯成二進制外還會生成一份文字檔案。左邊紅色部份是 C++源碼,中間紫色部份是文本格式的 .Wat 檔的內容,右邊藍色部份是 .wasm 檔的內容。

    多數情況下,人們把Wasm定義成 Web 上的程式語言,認為這是一個前端編程技術。其實這裏有一些的誤解,首先 Wasm 並不是一個新的"程式語言",沒有人會手寫 .wasm 檔來進行編程。 WebAssembly 有一套完整的語意,但作為開發者並不需要去了解它,開發者依然可以繼續使用自己熟悉的程式語言,由各個語言的編譯器將其編譯成 Wasm 格式後執行在瀏覽器內建的Wasm虛擬機器中 ,我認為 Wasm 更傾向於是一個套用在web場景中的編譯領域新技術。其次,Wasm 也並非只能執行在瀏覽器內,設計者對其抱有更加遠大的宏圖大業,這部份我們將在後面 Wasm 容器化這裏繼續展開討論。

    Mozzila 官方對 WebAssembly 的描述為:

    WebAssembly is a new type of code that can be run in modern web browsers — it is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++, C# and Rust with a compilation target so that they can run on the web. It is also designed to run alongside JavaScript, allowing both to work together.

    可轉譯為:

    WebAssembly 是一種可以在現代瀏覽器中執行的新型程式碼——它是一種低階的類似組譯的語言,具有緊湊的二進制格式,執行起來具有接近 原生的效能,其為 C/C++、C#和 Rust 等語言提供了一個編譯目標,以便它們可以在 Web 上執行。 它還被設計為與 JavaScript 一起執行,允許兩者一起工作。

    透過這段描述已經可以對 WebAssembly 有一個初步認識,我們再進一步給它拆開來看:

  • 首先,WebAssembly 是一門新的程式語言,它於2019年12月5日正式成為與 HTML、CSS 以及 JavaScript 並列的 Web 領域第四類程式語言。

  • 其次,WebAssembly 是"組合語言"而不是高級語言,程式設計師不直接編寫 WebAssembly 程式碼,而是透過特殊的編譯器將高級語言轉換成 WebAssembly 程式碼。

  • 再次,WebAssembly 是預處理過後的二進制格式,它實際是一個 IR(Intermediate Representation)!類似 Java 的 ByteCode 或者 .Net 的 MSIL/CIL。

  • 最後,WebAssembly 是 Web 上的語言,這意味著主流的瀏覽器可以讀取並且執行它。

  • 最後簡單總結,程式設計師依然還是編寫高級語言,然後透過「特殊的編譯器」生成 WebAssembly 二進制程式碼,最終 WebAssembly 程式碼再被一個嵌入在瀏覽器裏的"特殊的虛擬機器"執行。 這就是 WebAssembly 的全部工作過程。

    3.2 為什麽需要 WebAssembly?

    在 Web 領域,我們已經有了 JavaScript 這樣利器,但美中不足的是 JavaScript 的效能不佳,即使可以透過第二章裏提到的各種編譯最佳化來解決一部份問題,但在類似圖形影像處理、3D 遊戲、AR、VR 這些高效能套用的場景下,我們似乎任然需要一個更好的選擇。

    「快」是相對的,目前我們可以認為在執行速度上:原生 C/C++ 程式碼 > WebAssembly > asm.js > 原生 JavaScript 。其中 WebAssembly 比 asm.js 要快的原因在於:

    1. WebAssembly 體積更小,JavaScript 透過 gzip 壓縮後已經可以節約很大一部份空間,但 WebAssembly 的二進制格式在被精心設計之後可以比 gzip 壓縮後的 JavaScript 程式碼小 10-20% 左右

    2. WebAssembly 解析更快,WebAssembly 解析速度比 JavaScript 快了一個數量級 ,這也是得益於其二進制的格式。除此之外,WebAssembly 還可以在多核CPU上進行並列解析。

    3. WebAssembly 可以更好利用 CPU 特性,之前我們說到 asm.js 可以透過各種「奇技淫巧」來編譯最佳化,但其還是受限於 JavaScript 的實作。而 WebAssembly 可以完全自由發揮,使得其可以利用更多 CPU 特性,其中例如:64位元整數、載入/儲存偏移量以及各種 CPU 指令。在這一部份,WebAssembly 能比 asm.js 平均提速 5% 左右

    4. 編譯工具鏈的最佳化,WebAssembly 的執行效率同時取決於兩部份,第一個是生成程式碼的編譯器,第二個是執行它的虛擬機器。WebAssembly 對其編譯器進行了更多的最佳化,使用 Binaryen 編譯器代替了 Emscripten, 這部份所帶來的的速度提升大約在 5%-7%

    當然,速度上的提升並不是全部。WebAssembly 的意義在於開辟了一個新的標準,不再拘泥於 JavaScript 而是直接面向跟底層的機器碼。用任何語言都可以開發 WebAssembly,而 WebAssembly 又可以高效執行在任何環境下,這也是 Mozilla 的程式設計師對 WebAssembly 抱有的最遠大的宏圖大業。文章將在第六章對 WebAssembly 在非 Web 端的套用繼續展開討論。

    3.3 WebAssembly 與 JavaScript 執行效能詳細對比

    關於 WebAssembly 的效能,整體上我認為可以描述為「很快,但是不夠快」。也就是說, 我們期望它比 JavaScript 快非常多,快個10倍或者8倍,但實際上只能快一點點,大概也就是不到2倍左右 ,而且在不同的測試場景下差異可能會很大。也許你會說100%的效能提升已經很高了,但實際上這也許不能說服大量開發人員完全轉向一個嶄新的有學習成本的技術。

    Zaplib(一個高效能 Web 框架)的工程師從最大效能和標準效能兩方面對 WebAssembly 與 JavaScript 效能進行更詳細的對比,結論如下:

    3.3.1 最大效能(盡可能"奇技淫巧"地使用 JS)

    在最大效能上,特殊編寫的原生 JS 是可以跟 Wasm 大致持平的。其原因在於JS可以透過 ArrayBuffer 來模擬成一個"memory managed language":

    1. 可以盡可能避免掉自動 GC 的額外開銷。

    2. 可以對數據的局部性(cache locality)進行最佳化來提升緩存命中,從而提升數據讀寫的效率。(緩存局部性對陣列的效能很重要!)

    3. 當你盡可能避免掉其它開銷,只使用迴圈、局部變量、算術、函式呼叫的時 候,原生 JS 會非常快。

    舉個例子如下,這是一個計算多個2維向量平均長度的 TS 函式

    // Unoptimized Typescripttype Vec2 = { x: number, y: number };functionavgLen(vecs: Vec2[]): number{let total = 0;for (const vec of vecs) { total += Math.sqrt(vec.x*vec.x + vec.y*vec.y); }return total / vecs.length;a}

    這是使用了 ArrayBuffer 替換陣列了實作:

    // Optimized Typescript, using ArrayBuffersfunctionavgLen(vecs: ArrayBuffer): number{let total = 0;const float64 = newFloat64Array(vecs);for (let i=0; i<float64.length; i += 2) {const x = float64[i];const y = float64[i+1]; total += Math.sqrt(x*x + y*y); }return total / (float64.length / 2);}

    在範例中,ArrayBuffer 每 16 位儲存一個二維向量,前 8 位是向量 x,後 8 位是向量 y。後者程式碼的效能會遠高於前者,具體細節有興趣可以參考(
    https://zaplib.com/docs/blog_ts ++.html)。總而言之就是,可以透過 JS 的 ArrayBuffer 來手動管理 JS 記憶體,盡量避免掉效能開銷大的地方,剩下的普通指令的執行跟 Wasm 並無本質差異。除此之外,瀏覽器裏的 JS 相比 Wasm 在某些方面甚至還具有優勢:

    1. JS 可以存取一些零拷貝(zero-copy)的方法。例如 TextEncoder 和 FileReader.readAsArrayBuffer,而 Wasm 還需要額外再進行一次記憶體拷貝。

    而 Wasm 相比 JS 的優勢在於:

    2. SIMD 加速。SIMD.js 的 API 已經被棄用,取而代之的是 Wasm 的 SIMD 實作。

    3. 前置的編譯最佳化。

    3.3.2 標準效能(正常使用程式語言)

    對於實際情況而言,用標準的JS的進行效能對比才是有意義的,原因在於:

    1. 程式碼的編寫復雜度和可維護性也是很重要的,"奇技淫巧"並不適合生產工作中使用。

    2. 程式碼工程會依賴大量第三方庫,這些庫大機率都是標準 JS 來編寫的。

    如上圖,這個 3D 人物動畫是一個經典的 CPU 計算密集的測試用例,且可以直觀感受到效能在幀數上的表現(
    http://aws-website-webassemblyskeletalanimation-ffaza.s3-website-us-east-1.amazonaws.com/)。感興趣的同學可以在自己瀏覽器裏嘗試一下,當 3D 人物數量為100時 JS 版本會有明顯卡頓,切換到 Wasm 則不會有卡頓感。

    這是在 17 年 Wasm 誕生之初的測試,可以看到在不同的環境下 Wasm 比標準 JS 快了 8-15 倍。隨著 JS 的不斷最佳化,現在再去測試可能就不會有這麽大的差異了。更重要的是,這個測試用例不一定能代表真實的 Web 套用,真正的 Web 套用可能不會命中這麽多"最佳化項",8 倍以上的效能差異往往只存在於測試用例中。這裏我必須再重復一下就是,Wasm 快 10% 到 1000% 都有可能,不同的測試環境下不可一概而論。

    3.4 如何正確使用 WebAssembly?

    首先需要再次強調的是,WebAssembly 的誕生並不是要取代 JavaScript,Web 端整個主框架還是 HTML+JS+CSS 這一套。Web 套用的大部份基礎功能也依然是靠 JavaScript 來實作,我們只是將 Web 套用中對效能有較高要求的模組替換為 wasm 實作。在這樣的場景下,正確使用 WebAssembly 的步驟為:

    1. 整理 Web 套用中所有模組,梳理出有效能瓶頸的地方。例如你的 Web 套用裏有視訊上傳、檔對比、視訊編解碼、遊戲等模組,這些都是很適合用 WebAssembly 來實作的。相反,基礎的網頁互動功能並不適合用 WebAssembly 來實作。

    2. 進行簡單的 demo 效能測試,看是否能達到預期的加速效果。如果加速效果並不明顯,那麽就不適合切換到 Wasm。

    3. 確定用來編譯成 WebAssembly 的源語言,目前主流的語言基本都是支持 WebAssembly 的,唯一不同的區別是其編譯器的最佳化程度。如果你使用過 C++、RUST,最好還是用這兩種語言來編寫,其編譯最佳化程度會更高。當然了如果你想使用 PHP/GO/JS/Python 這些你更加熟悉的語言的話,也是不錯的選擇,畢竟有時候開發效率會比執行效率要更加重要。

    4. 編碼實作,然後匯出 .wasm 檔。這一步基本沒什麽難度,確定了語言之後使用對應的編譯器即可,需要註意的是記得盡量多開啟 debug 選項,不然有執行時報錯的話你就只能對著一堆二進制程式碼懵逼了。

    5. 編寫 JavaScript 膠水程式碼,載入 .wasm 模組。在最小可行版本的實作中,在 Web 上存取 WebAssembly 的唯一方法是透過顯式的 JavaScript API 呼叫,而在 ES6 標準中,WebAssembly 也可以直接從<script type='module'>的 HTML 標簽載入和執行。

    3.5 使用範例

    3.5.1 快速執行試驗

    看了剛才執行 WebAssembly 的步驟,是否覺得還是有些繁瑣呢?沒關系,這裏教你一個快速體驗執行 WebAssembly 的方法:

    1. 開啟任意的瀏覽器,例如 Chrome。

    2. 按 F12,啟動開發者工具。

    3. 找到 Console 頁簽,復制這一段程式碼,回車執行。

    WebAssembly.compile(newUint8Array(` 00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01 7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61 64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02 08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c 0f 0b`.trim().split(/[\s\r\n]+/g).map(str =>parseInt(str, 16)))).then(module => {const instance = new WebAssembly.Instance(module)const { add, square } = instance.exportsconsole.log('2 + 4 =', add(2, 4))console.log('3^2 =', square(3))console.log('(2 + 5)^2 =', square(add(2 + 5)))})

    這裏我們是透過直接手寫二進制機器碼的方式生成了一段 wasm 程式碼,並使用了 WebAssembly.compile 介面來進行編譯,最後呼叫了 wasm 實作的 add 和 square 函式。如果順利的話,你的瀏覽器會編譯這段 WebAssembly 程式碼 並呼叫執行,輸出對應的計算結果,具體如下圖所示:

    當然,如果如果沒有按預期輸出的話,那就說明你當前的瀏覽器版本是不支持 WebAssembly 的。

    WebAssembly 在 Web 端的套用

    一家名為"Scott Logic"的軟體開發商在2022年6月釋出了2022年 WebAssembly 現狀報告(這個統計允許開發者選擇多個選項,所以總和是大於100%的),在關於 WebAssembly 套用的統計中有幾個資訊值得關註:

    1. 首先 ,WebAssembly 最多的套用場景依然是在 Web 站點開發上,大約占65%。

    2. 其次 ,WebAssembly 在 Serverless 和容器化方面的套用大幅增加,由去年的20%提升到了35%。

    3. 最後 ,增長振幅最大的是在"作為外掛程式環境"套用場景,WebAssembly 的沙盒化安全環境很適合用於托管不受信任的第三方程式碼。

    本章會介紹一些公司內外的 Web 端套用場景,關於伺服端的套用會在第五章繼續介紹。

    4.1 常見 Web 端套用概覽

    4.1.1 【Google-視覺化】谷歌地球 3D 地圖

    在最早的版本 Google Earth 是只能跑在 Chorom 瀏覽器的,因為其底層用的是跟 WebAssembly 類似的 Native Client 技術。目前的 Google Earth 已經可以執行在 Firefox、Edge、Opera 瀏覽器,其關鍵的一點就是用WebAssembly 代替了原來 Native Client。

    4.1.2 【Bilibili-編解碼】嗶哩嗶哩視訊網站

    B站 視訊上傳的功能裏有大量的 Wasm 模組,類似視訊上傳、封面圖處理這些都是計算比較密集的場景。如上圖所示,B站 用到了 Wasm 版 FFmpeg 來加速視訊編解碼,這應該是 WebAssembly 最常見的套用了。除此之外還用到了Wasm 版 Tensorflow,這裏應該是用來實作 "AI智慧生成封面" 的功能。

    4.1.3 【Figma-設計工具】Figma 線上UI設計

    Figma 是近年來少有的可以稱得上擁有「矽谷速度」的創新型公司。2018年初,Figma 的估值才剛剛過1億美元,還僅僅是一個小眾設計工具,到了2021年,Figma 估值暴漲100倍來到了100億美元,其在設計圈的地位已經足以跟此前幾乎處於壟斷地位的 Adobe 產品抗衡,成為了產品圈、設計圈內人人必用的工具。

    Figma 可以說是典型的 WebAssembly 套用了,使用了 zaplib(一款基於 wasm 和 Rust 的高效能 Web 套用框架)來進行開發。外圍的互動操作還是用原生的 JS+CSS+HTML 來實作的,中間核心繪圖區域是一個由 wasm+webGL 來驅動的的 canvas 模組。

    4.1.4 【Adobe-設計工具】Photoshop Web 版

    就在幾年前,直接在瀏覽器中執行像 Photoshop 這樣復雜的軟體的想法還很難想象。然而,透過使用各種新的網路技術,Adobe 現在已經將 Photoshop 的公開測試版帶到了網路上。

    Adobe 工程師這裏所說的新技術,其中很重要一部份就是 WebAssembly。除了解決效能問題,更重要的是 Photeshop 的 Web 端和 PC 端套用可以由同一份源碼編譯生成。Adobe 使用 Emscripten 將 Photeshop 的完整 C++ 工程直接移植到了 Web 端,而無需用 JS 重寫。Emscripten 是一個功能齊全的工具鏈,它不僅可以幫你將 C++ 編譯為 Wasm,還提供了一個轉換層,可以將 POSIX API 呼叫轉換為 Web API 呼叫,將 OpenGL 轉換為 WebGL。

    4.1.5【Zoom-線上會議】Zoom Web版

    將 Zoom 移植到 Web 端,其復雜程度絕對不低於前面所說的幾個套用。除了視訊流的處理,Zoom 還提供了自動字幕、虛擬背景等功能,這些都是典型的 CPU 計算密集套用。ZoomWeb 的核心是 WebRTC,在 WebAssembly 誕生後, Zoom 的工程師將 WebAssembly SIMD 的能力引入了 ZoomWeb。WebAssembly SIMD 提供了可移植、高效能的 SIMD 命令集,可用於目前絕大多數主流 CPU 架構。音視訊編解碼、影像處理這些都是 SIMD 的典型套用場景,ZoomWeb 中虛擬背景的底層計算就是利用 WebAssembly SIMD 來實作的。

    4.1.6【Google-機器學習】TensorFlow.js

    TensorFlow.js 是一個 JavaScript 庫,用於在瀏覽器和 Node.js 訓練和部署機器學習模型。在 2020 年,TensorFlow.js 引入了一個新的 WebAssembly 加速後端。從 TensorFlow.js 2.3.0 版開始,Wasm 後端透過 XNNPACK 利用 SIMD 指令和多執行緒,速度提高了 10 倍,其中 XNNPACK 是一個高度最佳化的神經網路運算子庫。

    TensorFlow.js 從 2.1.0 開始支持 SIMD,從 TensorFlow.js 2.3.0 開始支持多執行緒。Wasm SIMD 是 wasm 標準第 3 階段的提案,Wasm threads 是 wasm 標準第2階段的提案,目前絕大多數瀏覽器環境都可支持該兩種能力。SIMD 和多執行緒的效能增益彼此獨立。TensorFlow 的基準測試表明,SIMD 為普通 Wasm 帶來了 1.7-4.5 倍的效能提升,而多執行緒在此之上又帶來了 1.8-2.9 倍的加速。

    4.1.7 【FFmpeg-音視訊處理】

    FFmpeg 就不用多介紹了吧,20多年前 Fabrice Bellard 發起的 FFmpeg 計畫不知道養活了多少公司和音視訊開發者。XX 播放器,XX 格式工廠基本都是在 FFmpeg 上面套了個 UI。ffmpeg.wasm 的意義就在於可以不再完全依賴瀏覽器的音視訊能力,強大的幾乎支持所有格式的音視訊處理能力可以被移植到 Web 端。根據目前了解到的資訊,FFmpeg 在作業系統、硬體、驅動等環境支持的情況下,是可以利用 GPU 或者其它硬體來加速解碼的。大多數瀏覽器也都支持硬體編解碼加速,但執行在瀏覽器內的 ffmpeg.wasm 應該是只能純 CPU 軟解的,這裏可能會存在一定的效能問題。

    4.1.8【Unity/Unreal-遊戲引擎】H5 遊戲開發、Web 端遊戲營運工具

    這是 Unity 在4年前釋出的一個 demo,使用Unity開發並釋出到 Web 端。其遊戲效果已經很好了,且可在瀏覽器裏流程執行。使用原始 HTML5 技術如果想達到跟這個 demo 一樣的體驗和效能,投入的成本將會非常大。目前所有版本的 unity 以及 Unreal4.18 之前版本的 UE,都支持將遊戲內容打包釋出到 Web 端。在 Unity 裏的平台名叫"webGL",在 UE 裏則是"HTML5"。將遊戲內容釋出到 Web 端,主要需要解決3個問題,首先是將引擎的底層程式碼和指令碼程式碼編譯成 wasm 的方式來執行,其次引擎的"平台無關層"需要適配 webGL 這個圖形 api,最後則是適配瀏覽器的系統介面。在 wasm 未誕生之前,引擎則是將程式碼轉成 asm.js 來執行。至於 UE 為什麽在後面的主線版本不再支持 Web 端,官方給個說法是"未達到預期效果且不好維護"。

    WebAssembly 在伺服端的套用

    看到這裏你也許會覺得疑惑,WebAssembly 不是跑在瀏覽器裏的前端技術?為什麽能跟伺服端的 Docker、K8S、容器化這些概念扯上關系?就像之前文章說到的,這絕對不是一個僅限於前端的新技術,WebAssembly 有著更遠大的的宏圖大業。

    Docker 的創始人 Solomon Hykes 在 2019 年 3 月份釋出了一條 Twitter 引起了眾多討論,譯文如下:

    如果2008年的時候,WASM 和 WASI(WebAssembly System Interface, WASM系統介面)這兩個東西已經存在了的話,我們就沒有必要創立 Docker 了。在伺服器上執行 WebAssembly 是計算的未來,目前缺少的就是一個標準的系統介面,希望 WASI 能夠彌補上這塊缺失的拼圖。

    5.1 WASI:解決跨平台執行作業系統的差異

    如下圖所示:WebAssembly 執行在瀏覽器內,與系統互動靠的是 JS 膠水語言的能力,JS 透過瀏覽器內核再到作業系統內核。而 WebAssembly 脫離了瀏覽器後,執行在各個作業系統中也需要抹平系統 api 的差異性,這就是 WASI 需要解決的問題。

    WASI(WebAssembly System Interface, WASM 系統介面),這裏的系統介面指的就是例如檔操作、網路連線、系統時鐘、隨機數之類的作業系統呼叫,開發 WASI 的唯一目的就是將 WebAssembly 向瀏覽器之外推進,最終能夠真正做到一份 wasm 程式碼執行在所有不同環境不同作業系統的機器中。

    例如 C 這樣的語言可以跨平台執行,這實際上是源碼級的跨平台,一次編寫多次編譯,編譯器根據目標平台選擇對應的系統 api。如下圖所示:C 源碼被 clang 編譯了3次,生成了三份對應不同目標平台的機器碼。

    wasm 是二進制級別的跨平台,這種可移植性讓使用者分發程式碼更容易。wasm 只需要被提前編譯一次,就能在不同作業系統上執行。在編譯的時候並不確定其目標平台,wasi 這裏實際需要的是一個跨平台的 runtime!如下圖所示:C 源碼只被編譯了1次,.wasm 透過 WebAssembly runtime 執行在不同系統中。

    看到這是不是有種熟悉的感覺了?因為 Java 也就是這麽幹的,WebAssembly runtime 對應的就是 JVM,.wasm 則對應 java bytecode。所不同的是, WebAssembly 支持了更多的語言,而且執行在瀏覽器裏支持更加完備。

    5.2 WASI現在的進展?

    WASI 實際上是一個標準,目前最主流的實作方案是 Bytecode Alliance 使用 Rust 開發的 Wasmtime。截止到我寫這篇文章的時候已經有11.3K的 star。看了最新 git 記錄,整個開發應該是仍然處於"瘋狂打碼中"的狀態。

    在2022年9月,Bytecode Alliance 釋出了 Wasmtime1.0:

    快!安全!能夠用於生產環境!這就是開發團隊對1.0版本最直接的介紹,如果說以前 WASI 還處於探索階段,這個版本的推出已經意味著 WASI 可以在生產環境進行更多的嘗試了,整個社群目前也是非常的活躍。

    5.3 WebAssembly 在伺服端的套用

    在雲端運算的概念裏,伺服端的容器虛擬化大概可以劃分為三個不同的抽象層:

    1. Hypervisor VM,或者又稱 microVMs,其是最底層的虛擬方案,能夠直接與硬體進行互動。常見案例有:AWS Firecracker、VMware。

    2. 在往上一層是 Application containers,所熟知的 Docker 就在這一層,依然是比較"重"的虛擬方案。

    3. 最上層是 High level language VMs,JVM、Python runtimes 以及 WebAssembly 都屬於這一層。

    那麽在伺服端,WebAssembly 到底可以套用在哪些方面?其優勢是什麽呢?官方給出的建議是有以下五個場景是比較適合的:

    一、微服務/無服務平台 ,WebAssembly 是非常適合用作微服務和無服務平台的。後端即服務(Backend as a Service,BaaS),函式即服務(Function as a Service,FaaS)都可以歸屬到 severless 無服務模型。WebAssembly 的啟動時間相比 Docker 或者其它 VM 要快很多,WebAssembly 的執行時是非常"輕"的, 啟動一個 WebAssembly 例項只需要5微秒 。除此之外,輕量級所帶來的另外一個優勢就是 可以在一台機器上搭載更多例項

    二、第三方外掛程式系統 ,當平台需要執行第三方開發者的程式碼,安全性就是不可避免的問題。而 WebAssembly 是沙盒化的 ,並且第三方程式無法存取未明確交給給它的任何系統資源。除此之外,平台和第三方外掛程式之間的通訊也是很快的。

    三、為資料庫實作 UDF 功能 ,UserDefineFunction(使用者自訂函式,UDF)是資料庫應用程式加速的一種方法。指的是將邏輯程式碼放到資料庫中執行,透過降低應用程式和資料庫之間的互動開銷來提升整個程式的執行效率。例如 Google BigQuery 允許使用者從 SQL 查詢呼叫以 JavaScript 編寫的程式碼,阿裏雲的 MaxCompute 可以直接將 Java 或 Python 程式碼作為 UDF 嵌入 SQL。資料庫可以基於 WebAssembly runtime 來實作 UDF 能力,其優勢在於: 支持更多語言、安全隔離、跨平台、效能好、冷啟動快等。

    四、搭建可信執行環境 Trusted execution environments (TEEs)指的是為不想或者不能信任底層系統的應用程式單獨開辟一個在 CPU 上安全執行環境,此時的 TEE 應用程式獨立於其它作業系統、虛擬環境、內核以及其它系統軟體。TEE技術常用於移動支付、私密計算等安全性要求較高的場景。使用WebAssembly搭建 TEE 的優勢在於: 支持更多語言、WebAssembly 執行時支持大多數主流 CPU 架構。

    五、開發可移植的應用程式, 借助跨平台的 WebAssembly runtime,WebAssembly 應用程式可以執行在不同 CPU 架構、不同作業系統的電腦上。開發者只需要專註於程式的邏輯功能,而不需要過多擔心平台的差異性、效能、安全等問題。



    總結

    第一個問題:你的 Web 套用效能瓶頸在哪裏? 先想清楚這個問題再做最佳化本文的主角並非 webGL,但是文章裏多次不可避免的提到,其根本原因就在於 Wasm 解決的是 Web 端 CPU 計算密集的效能問題,而效能瓶頸可能壓根就不在這裏。Figma 就是最典型的例子,他們使用wasm將套用移植到 Web 端,並對 Web 端的效能進行了大量最佳化,但最後復盤發現效能提升的真正來源其實是 webGL 渲染器的改進,也就是 GPU 硬體加速的收益,顯然這跟 WebAssembly 並沒有任何關系。

    在之前我們有提到,Unreal 在4.23版本之後將 Web 端的支持從主線分支移除。但近期有一家叫做"Wonder Interactive"創業公司又將這部份能力彌補了回來,並且將在5月份的洛杉磯 GamesBeat 進行宣講。在他們計劃的工作裏,UE5 的 Web 端支持將對接最新的 WebGPU 來實作,遊戲 AI、尋路等場景也可以用 WebGPU 新增的通用計算(GPGPU)介面來加速。除此之外,遊戲資源的壓縮、下載和載入也都需要被考慮,WebAssembly 提供的能力也只是整個流程中的一個環節。

    第二個問題:現在已經是2024年了,WebAssembly 到底算成功了嗎? 如果要從技術的成熟度上來說,我認為是成功的,WebAssembly 已經投入到了大量的生產套用中。

    如果要從推廣套用的角度來說,我認為目前是不成功的,因為90%以上的場景其實不需要 WebAssembly。

    另外一個角度來說就是, WebAssembly 很快,但是還不夠快,不足以讓開發者完全轉向擁抱一個嶄新的技術。

    第三個問題:我們如何擁抱 WebAssembly 這個新技術? 有3個地方我覺得還是很有套用價值的:

  • PC 端的套用移植到 Web 端,無需二次開發,保持多端程式碼一致性。

  • 音視訊處理這些高效能套用的場景,切換到 WebAssembly 確實能帶來很大的效能提升。

  • 後端微服務/無服務這一塊,可以實作支持多語言的雲函式之類的平台。

  • 簡單來說就 WebAssembly 並不是什麽神奇的技術,更像是當年 JVM 未完成理想的開源 plus 版本,作為開發者沒必要跟風追捧或者詆毀。 不同的測試用例、硬體環境、編譯器最佳化程度、瀏覽器引擎最佳化程度都會對 Wasm 的執行產生影響,不在具體場景下空談效能都是沒有意義的,根據自己的套用場景+效能測試結果+改造工作量綜合評估是否使用即可。

    本文經授權轉載「騰訊雲開發者」,點選「閱讀原文」直達原文。