WebAssembly 在白鷺引擎5.0中的實踐

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

 

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

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

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

WebAssembly 的生成原理github

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

首先經過 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必定會有一個光明的將來。

相關文章
相關標籤/搜索