WebAssembly 在白鷺引擎5.0中的實踐 原 薦

作爲一種可移植、體積小、加載快且兼容web的全新格式,WebAssembly受到諸多關注,並迎來企業的探索實踐。白鷺引擎利用WebAssembly重新實現了一個新的渲染內核並作爲一個可選項提供給開發者,使得白鷺引擎5.0成爲業內首個雙核驅動的引擎。在此過程中積累了一些經驗,白鷺引擎首席架構師王澤今天和大家一起分享背後的故事。

 

WebAssembly 是 Google Chrome、Mozilla FireFox、Microsoft Edge、Mozilla FireFox 共同宣佈支持、並在 2017年3月份在各自瀏覽器中提供了實現的一種新技術。他被設計爲一種可移植的、安全的、低尺寸的、高效的二進制格式。瀏覽器可以解析並運行這種格式,並擁有比 JavaScript 更高的性能和解析速度。WebAssembly 可以通過編寫 C / C++ 代碼,通過專門的編譯器生成 .wasm 格式的文件,直接運行在最新的瀏覽器中。

白鷺引擎是一款 HTML5 遊戲引擎,他提供了遊戲開發所需要的諸多功能,並允許開發者編寫的遊戲運行在 Web 瀏覽器或移動應用的 WebView 容器中。

在白鷺引擎 5.0 中,我們使用 WebAssembly 重新編寫了白鷺引擎的渲染核心,以便進一步提升渲染效率。在這個過程中,白鷺引擎遇到了WebAssembly的各種問題,在此與讀者進行一些 WebAssembly 在實踐中遇到的問題及解決方案,希望對計劃或者正在使用WebAssembly 的開發者有所幫助。

WebAssembly 的生成原理

上圖展示瞭如何通過編寫 C / C++代碼生成 WebAssembly 內容。

首先通過 LLVM ,將 C/C++ 源代碼編譯爲 LLVM bytecode。這 是一種跨語言的底層虛擬機字節碼,理論上所有強類型編程語言均可以生成這種字節碼。通過這一點可以得知,在未來理論上所有強類型編程語言(諸如 Java / C# 等)均可以開發 WebAssembly 程序。

其次,通過 Emscripten 中的後端編譯器,將這種抽象字節碼生成 asm.js 格式的文件。這是一種特殊的 JavaScript 代碼,一些 JavaScript 引擎會將這種格式以比通常的 JavaScript 代碼更快的速度運行,並且由於 asm.js 仍然是 JavaScript,所以哪怕 JavaScript 引擎不支持該特 性,也會以通常的方式運行這段邏輯。這意味着使用 C/C++編寫的源代碼,哪怕用戶設備不支持 WebAssembly,也可以回退到 JavaScript 運行並得到一致的結果。

接下來,asm.js 會通過另一個編譯器生成爲 WebAssembly 的 .wasm 文件,由於 WebAssembly 是二進制格式,相比 JavaScript 而言,其代碼體積同比小很多,並且由於已經是面向機器碼的格式,也無需在運行前對源代碼耗費時間進行 JIT 編譯操作。

通過上述內容可以看出,WebAssembly 理論上可以通過任何強類型語言生成,不強制依賴用 戶的本地運行環境,代碼體積小、解析速度快,幾乎是徹底解決了 JavaScript 的各種頑疾。

WebAssmely 項目入門

開發環境配置

介紹完 WebAssembly 的機制原理,接下來筆者介紹一下如何使用 WebAssembly 開發第一個 HelloWorld 程序。

如果您想開發 WebAssembly,強烈建議您收藏一下三個站點:

 

WebAssembly 官網:https://webassembly.org/

WebAssembly MDN:WebAssembly

Emscripten 官網:Main - Emscripten 1.37.22 documentation

 

在具體的開發中遇到的問題,大部分在這三個網站中可以找到答案。

 

首先,進行項目開發前需要配置 WebAssembly 開發環境,筆者以 Windows 爲例,MacOS 與 Linux 開發者可以閱讀 Emscripten 官網文檔。

在 Windows 中,可以直接從 Emscripten 官網下載 EmscriptenSDK,安裝後,在命令行輸入 emcc -v,可以看到顯示當前版本號爲 1.35.0。爲了保證最佳的開發體驗,我們需要手動升級 EmscriptenSDK 到最新版本,執行以下命令:

# 獲取當前版本信息

emsdk update

# 安裝最新版本,筆者目前爲 1.37.14

emsdk install latest

# 使用最新版本

emsdk activate latest

在安裝過程中,由於需要下載文件,考慮到國內的特殊網絡環境,有時下載會失敗,讀者可以根據下載時候的日誌輸出,提前將要下載的文件放置於正確路徑,然後再執行安裝命令。

編寫 HelloWorld 應用

在保證 Emscripten 處於最新版本後,就可以開始編寫 HelloWorld 應用了。

創建一個新的 C 文件,名爲 main.c,編寫以下內容

#include <stdio.h>

int main() {

printf("hello, world!\n");

return 0;

}

然後在終端中執行以下命令emcc main.c -o out/index.html最終會生成以下項目結構

project-root

|-- main.c

|-- out/index.html

|-- out/index.js

讀者應該已經發現,生成的代碼並不包含 WebAssembly 的 wasm 格式文件,而是一個名爲 index.js 的 asm.js 五年。這是因爲 Emscripten 最初是爲了生成 asm.js 格式而設計的。爲了生成 wasm,需要額外添加一個參數 emcc main.c -o out/index.html -s WASM=1,當添加這個參數後,Emscripten 會再通過一個名爲 Binaryen 的編譯器將 asm.js 格式轉換爲 wasm 格式。

細心的讀者可能會發現,理論上 Binaryen 無需 asm.js 這個中間格式,而應該是直接從 C++ 生成的 LLVM 去直接輸出 wasm 格式,目前 Binaryen 已經支持了這種方式,但是目前還在測試階段,所以默認行爲仍然是通過 asm.js 作爲中間層。

添加完上述參數後重新執行,就會發現項目中生成了名爲 index.wasm 的文件,運行 index.html,可以看到屏幕上輸出了 Hello,World。

與 JavaScript 進行交互

除了標準C之外,Emscripten 提供了大量函數,用於 JavaScript 、HTML 與 WebAssembly 進行通訊,其最簡單的代碼如下所示:

#include <emscripten.h>

int main() {

EM_ASM( alert("hai"));

return 0;

}

通過引入 emscripten.h 頭文件,就可以調用這些函數,上述代碼中展示瞭如何在 WebAssembly 中直接調用 JavaScript 內容。

爲了簡化調用,Emscripten 提供了 EMSCRIPTEN_BINDING 等API,可以將一個 C++ 類和函數與 JavaScript 進行直接綁定。

由於 WebAssembly 與 JavaScript 的調用存在着一定的性能問題,所以更推薦開發者使用 typed_memory_view 的方式,將 WebAssembly 中的一段內存與 JavaScript 的一段 TypedArray進行綁定,通過這種方式,WebAssembly 與 JavaScript 的調用不是通過拷貝數據、而是直接對內存進行共享的方式進行交互。通過靈活運用這種方式,可以大幅提升性能,具體一些實際案例可以參見下文的“白鷺引擎的 WebAssembly 實踐”瞭解更多信息。

白鷺引擎的 WebAssembly 實踐

在網頁端運行一款遊戲的幾種方式

通過瀏覽器插件機制,在網頁插件中運行遊戲,如 Flash Player、Unity Web Player 等。這種機制的優勢是由於插件本身使用 NativeCode 對遊戲組件進行了許多封裝,所以運行效率很高,缺點則是需要瀏覽器支持,而現在瀏覽器更加傾向於無插件化。

其次是遊戲邏輯和遊戲引擎均交由 JavaScript 進行處理,最終渲染則通過控制 DOM 節點或者 操作 DOM-Canvas 相關 API去實現。這種方式實現了無插件化,但是由於 JavaScript 自身性能存在瓶頸,性能也有一定的侷限性。目前市面上絕大多數 HTML5 遊戲引擎(包括白鷺引擎)均是如此實現,擴展到 WebApp 開發行業,無論是 Angular、React還是其他諸多框架的核心架構也是如此。

由於 WebAssembly 的引入,一些大型遊戲引擎廠商,比如 Unity3D,開始嘗試將其遊戲源代碼編譯爲 WebAssembly,運行瀏覽器中,這種做法理論上可以把大量基於C/C++編寫的遊戲發佈爲 HTML5 版本,但由於 HTML5 遊戲本身的資源加載機制與客戶端遊戲完全不同,直接轉換的遊戲仍然需要改造很多邏輯去適應網頁端“邊加載邊進行遊戲”的需求,否則當用戶進入遊戲時,需要加載上百兆的遊戲資源才能進入遊戲,這帶來了極其糟糕的體驗,並且很佔用內存。

由於將整個客戶端遊戲直接發佈爲 WebAssembly 格式目前並不成熟,所以我們認爲把遊戲中性能消耗較大的部分轉爲 WebAssembly,而將需要強調開發效率的部分繼續使用 JavaScript 是一種靈活的方式。

在上述四種方案中,主要是後兩種採用到了 WebAssembly 技術,在目前來看,由於第四種方案較爲穩妥,所以白鷺引擎採用了這種方案,在最新版本5.0中提供了基於 WebAssembly 的渲染內核,而遊戲邏輯本身仍然運行在 JavaScript 環境中。

JavaScript 與 WebAssembly 互操作性能很差

以白鷺引擎5.0的渲染庫爲例,白鷺引擎對外提供 JavaScript API,開發者編寫的 JavaScript 邏輯代碼會彙總爲一組命令隊列發送給 WebAssembly 層,然後 WebAssembly 建立對渲染節點的抽象封裝,並在每一幀對這些渲染節點進行矩陣計算、渲染命令生成等邏輯,最終生成一組 ArrayBuffer 數據流,最後 JavaScript 對這組數據流進行簡單的解析並直接調用 DOM 的WebGL 接口,把數據流傳遞給瀏覽器層。

這個過程中存在着幾個性能瓶頸:

首先是,由於 JavaScript 與 WebAssembly 的對象綁定後、互相調用的性能很差,這大大限制了WebAssembly的適用範圍,簡單的將特定幾個函數編譯爲 WebAssembly,然後交由 JavaScript 去調用的方式反而會因爲頻繁的互相操作反而造成性能下降。爲了繞過這個問題,WebAssembly 設計了一組 API ,可以用於將一段 JavaScript ArrayBuffer 與 WebAssemly 中的字節流進行共享操作。所以白鷺引擎將所有對 WebAssembly 的調用封裝爲了一組字節流命令,並在用戶邏輯全部執行完之後,將這個字節流命令傳遞給 WebAssembly,這樣就大幅減少了 JavaScript 和 WebAssembly 之間的互操作。

其次是,由於 WebAssembly 不能直接操作 WebGL 等瀏覽器 API ,所以在每一幀對渲染內容進行完計算之後,需要把計算結果再保存在一段字節流中,共享給 JavaScript,交由 JavaScript 去操作DOM節點。由於最終仍然是 JavaScript 去操作DOM節點,必然仍然存在一定的性能問題。無法操作 DOM 節點使得目前 WebAssembly 無法完全代替掉 JavaScript。這一問題在 WebAssembly 的路線圖中有所提及,會在未來的版本中加以解決。

因此可以看出,WebAssembly 適合將一段大量的、密集的邏輯計算抽象出來,統一一次性輸入所有的參數、一次性返回所有的輸出,比如遊戲主渲染循環、物理引擎、粒子系統、骨骼動畫計算等內容。

WebAssembly 的二進制格式可調試性較差

其次是可調試性,WebAssembly 被設計爲了一種開放的、可調試的程序,但目前無論是 Chrome 還是 FireFox ,在調試方面還有很大的提升空間。由於在目前階段調試較爲困難,所以用 WebAssembly 編寫業務邏輯代碼對研發來說還是很不方便的。目前白鷺引擎的策略是把 Emscripten 中的 API 與業務邏輯進行隔離,通過C++自身的開發環境,剝離 Emscripten 進行獨立的調試,然後再發布爲 WebAssembly 格式,而非直接在瀏覽器端調試 WebAssembly。

雖然目前可調試性較差,但是我們相信這個問題在未來一定會得到較好的解決,同時,由於二進制的原因,代碼體積很小,白鷺引擎團隊將大約300k左右(壓縮後)JavaScript 邏輯改用 WebAssembly 重寫後,體積僅有90k左右。雖然使用 WebAssembly 需要引入一個50k-100k的JavaScript類庫作爲基礎設施,但是總體來看資源尺寸的優勢還是很大的。

由於代碼格式是二進制、無法直接在瀏覽器中看到源碼,儘管理論上仍然可以通過逆向工程一定程度上得到原有的業務邏輯,但是由於開發者可以在編譯時使用了-O3 等激進的優化策略,所以最終反編譯得到的業務邏輯也是很難閱讀的。雖然理論上一切在客戶端的內容都是不安全的,但是與所有代碼都直接暴露給用戶相比,代碼安全性得到了很大的改善。

WebAssembly 的瀏覽器支持率仍很低

在當前,Chrome 57+ (包括PC與 Android),iOS 11 Safari 、FireFox 52 與 Microsoft Edge 均已支持 WebAssembly。但是仍然存在不穩定現象。以 Chrome 瀏覽器爲例,Chrome 57 支持 WebAssembly 的 MVP 版本,但是在 Chrome 58 上,大量的 WebAssembly 程序會直接導致進程崩潰,雖然後續的 Chrome 59 已經修復了絕大部分問題,但是仍然不得不對目前版本的穩定性持保留態度。

在不支持 WebAssembly 的瀏覽器中,由於 C++代碼在編譯 WebAssemly 的同時也可以編譯出完全符合 JavaScript語法的asm.js,所以可以保證業務邏輯是可以通過這種方式回退支持所有的瀏覽器。

WebAssembly 在移動設備上性能並沒有跨越式提升

除此之外,筆者經過測試發現,在 PC Chrome 上,WebAssembly 相比 JavaScript 的性能有很大提升,但是在 Mobile Chrome 上,提升目前只有30%左右,這說明目前 WebAssembly 自身在性能挖掘上還有很大空間。

筆者運行了一個複雜的測試用例,15000個顯示對象在屏幕上進行旋轉,其測試結果如下:

從上性能測試可以看出,WebAssembly 比 JavaScript 版本以及 asm.js 版本均有一定提升。由於在測試Demo中,遊戲邏輯(每一幀遍歷15000個顯示對象,修改其旋轉屬性)無論任何版本中均處於 JavaScript 環境運行。所以遊戲邏輯的開銷三種版本是一致的,而使用 WebAssembly 實現的渲染邏輯比 JavaScript 版本快30%以上。

在運行 benchmark 等極限測試時,遊戲引擎使用 WebAssembly 並不比 JavaScript 有成倍的提升。筆者的推論是:由於 JavaScript 引擎的JIT機制會把經常運行的函數進行極限的編譯優化,所以在 benchmark 這種代碼大量反覆執行的測試環境下,無論是 JavaScript 版本,還是 WebAssembly 版本,運行的都是高度優化後的機器碼,雖然 WebAssembly 版本仍然比 JavaScript 版有一定的性能優勢,但是並不明顯。而在運行業務邏輯代碼時,由於大部分業務邏輯代碼只運行一次,所以 JavaScript 引擎只會 對這部分代碼進行簡單的編譯優化而非極限優化,所以運行這一部分代碼 WebAssembly 相比 JavaScript 版本而言提升巨大,但是因爲上文所述,不建議開發者在編寫業務邏輯時使用 WebAssembly,所以這裏陷入了一個兩難。在目前而言,理想情況是除了底層庫之 外,部分關鍵的涉及性能問題的邏輯也可以使用 WebAssembly 進行編寫。

結論

綜上所述,目前爲止由於 WebAssembly 還不是非常完善,所以它目前的主要作用是作爲 JavaScript 生態的有益補充,與JavaScript共存而不是取而代之。但是通過其路線圖我們可以 得知,WebAssembly 的設計思想非常優秀,目前所有存在的問題從長遠的角度來說都是可以 解決的問題。在加上 WebAssembly 是非常罕見的由四大瀏覽器廠商共同宣佈會大力支持並 實現的功能,其瀏覽器兼容性問題也終究可以得到解決,再退一步,哪怕舊式瀏覽器不支持, 由於 WebAssembly 支持回退到 JavaScript,也可以保證正常運行。

筆者認爲,WebAssembly 就像當初的 HTML5 標準一樣,在公佈之後最開始不被很多人看 好,認爲會有瀏覽器兼容性問題、各大瀏覽器廠商的實現問題、性能問題、用戶需求與用戶體驗問題,但在近年來 HTML5 終於得到了廣泛的使用,甚至有些人認爲他可以在很多場景下取 代 NativeApp ,而非僅僅是當年“取代Flash”這一小目標。憑藉着底層技術的跨越式發展, 以及瀏覽器廠商的一致支持,WebAssembly一定會有一個光明的未來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章