WebAssembly是解決JavaScript 痼疾的銀彈?
寫在前面
《沒有銀彈》是 Fred Brooks 在 1987 年所發表的一篇關於軟件工程的經典論文。該論文的主要論點是,沒有任何一項技術或方法可以能讓軟件工程的生產力在十年內提高十倍。 在 Web 開發這一領域,由於 JavaScript 一直存在著諸多從本質上來看無法解決的問題,那麽解決 JavaScript 痼疾的銀色子彈是否存在呢?
聊聊JavaScript發展歷史
作為一門僅用了十天進行設計的語言,Brendan Eich 一定沒有想到 JavaScript 會從一門簡單的”腳本“發展成為 Web 開發的主宰,並且如火如荼的滲透到桌面或移動客戶端開發甚至服務器端開發中。之所以 JavaScript 如此火爆,甚至有了著名的 Atwood 定律,聲稱"任何可以使用 JavaScript 來實現的應用都最終都會使用 JavaScript 實現",主要原因在於兩點:
-
Web 應用可以越來越多的代替傳統的客戶端應用程序;
-
JavaScript 引擎的運算速度大幅改進。
從辯證的角度來看,上面兩個觀點其實是相互影響、相互促進的關系。JavaScript 引擎運算速度的逐步提升,導致了一部分簡單的客戶端應用可以被 Web 應用代替,進而用戶和開發者都希望更多復雜的客戶端應用也用 Web 實現,進而促進 JavaScript 引擎運算速度的進一步提升。
Google V8 引擎是 JavaScript 引擎性能改善的現象級項目。V8 於 2007 年發布,由於采用了 JIT ( just-in-time)技術,V8 引擎可以將 JavaScript 代碼在運行前編譯為機器語言,這樣運行速度就會有大幅提升。客觀存在的問題是,開發者對性能的要求是近乎無止境的,而基於 JIT 的設計思路帶來的性能已經逐漸被挖掘到了極限,並且逐步暴露出了一些難以解決的問題。首先是被稱為“重優化”的性能瓶頸。編譯為機器代碼的前提條件是必須讓編譯器清晰的知道變量的類型,偏偏 JavaScript 是一門弱類型語言,參考如下代碼:
function sum ( a , b ) { return a + b; } sum(1,2); sum("1","2");
V8 引擎在運行這段代碼時,在第一次調用 sum 函數時,由於傳遞的類型是兩個數字,所以會將 sum 這個函數的參數設置為數字類型並編譯為機器碼,但是緊接著,sum 函數又傳遞了字符串類型,這就導致編譯器只能講剛編譯好的 sum 函數拆解,然後重新將其編譯為參數類型為字符串類型的機器碼。這種情況大大降低了 JavaScript 的運行性能。
其次是因為編譯器的一些優化策略可能“弄巧成拙”,導致在特定情況下性能反而有負面影響。一個典型的例子是 TypeScript 編譯器在編譯代碼時的性能優化:
https://github.com/Microsoft/TypeScript/pull/10270 通過這個優化可以看到,通過將一個對象添加 delete 操作,強制關閉該對象的“隱藏類”機制,將一個對象切換為字典模式,達到了性能提升的作用。
再次是 JavaScript 具備垃圾回收機制,雖然 V8 編譯器已經對垃圾回收機制的算法進行了諸多的優化,但是在應用內存占用較大時,垃圾回收的瞬間明顯仍然還有卡頓現象,這導致了復雜應用有可能出現不定時的卡頓現象。這些問題都反映出,JavaScript 這種語言機制本身的靈活性,反而限制了 JavaScript 引擎的性能優化空間,如果希望徹底解決這一問題,必然需要拋棄 JavaScript 這門語言本身,采用一門強類型的編程語言才能達到最極致的性能,在這種技術思想的指引下,WebAssembly 技術應運而生。
提到了 WebAssembly,就必然首先提及對其有深遠影響的 asm.js,這是 Mozilla 在 2013 年推出的一項新技術,它是 JavaScript 的一個子集,舍棄了大量會導致性能問題的語法,並且被設計為通過 C / C++ 代碼編譯生成,而非手工編寫 asm.js 代碼。上述的 sum 函數在 asm.js 中表現為:
function sum ( a ,b ) { a = a | 0; b = b | 0; return ( a + b ) | 0; }
上述代碼中,標準的 JavaScript 引擎會對其進行解析,並生成正確的結果,而 asm.js 會根據一些不會對運行時造成計算結果錯誤的特殊標識對變量的類型進行聲明(比如 a = a | 0 表示變量 a 是一個整數),通過這種方式,這種代碼既可以在支持 asm.js 的 JavaScript 引擎上得到很高的性能,也會在不支持的設備上繼續按照正確的邏輯進行執行,而非無法運行。雖然如此,asm.js 仍然存在著一些問題,主要是基於 JavaScript 語法的文本格式解析速度不夠快,並且代碼尺寸偏大,為了解決這些問題,將 asm.js 進行二進制化的 WebAssembly 應運而生。
WebAssembly是什麽?
WebAssembly 是一種接近機器語言的跨平臺二進制格式。2017 年 3 月份,四大主流瀏覽器廠商 Google Chrome、Apple Safari、Microsoft Edge 和 Mozilla FireFox 均宣布已經於最新版本的瀏覽器中支持了 WebAssembly 的初始版本,這意味著 WebAssembly 技術已經實際落地、可以在特定生產環境進行嘗試。WebAssembly 目前可以通過 Emscripten SDK 生成,下圖是 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 理論上可以通過任何強類型語言生成,不強制依賴用戶的本地運行環境,代碼體積小、解析速度快,幾乎是 Web 開發未來的一顆“銀色子彈”。可惜的是在現階段,WebAssembly 仍然存在著不少問題需要去解決。
- 首先是自身的穩定性,以 Chrome 瀏覽器為例,Chrome 57 支持 WebAssembly 的 MVP 版本,但是在 Chrome 58 上,大量的 WebAssembly 程序會直接導致進程崩潰,雖然後續的 Chrome 59 已經修復了絕大部分問題,但是仍然不得不對目前版本的穩定性持保留態度。
- 其次是可調試性,WebAssembly 被設計為了一種開放的、可調試的程序,但目前無論是 Chrome 還是 FireFox ,在調試方面還有很大的提升空間。由於在目前階段調試較為困難,所以用 WebAssembly 編寫業務邏輯代碼對研發來說還是很不方便的。
還有就是與 Web 的互操作性。目前 WebAssembly 類似 WebWorker ,只能進行單純的數值計算工作,不能在 C++ 層直接操作 DOM 節點。雖然在未來路線圖中提及這一特性會在後續加入,但是在目前階段 WebAssembly 更適合被用於更純粹的密集型數據計算工作,而非直接編寫業務邏輯。
綜上所述,在目前階段,WebAssembly 不適合直接編寫具體的業務邏輯,而更適合編寫應用程序中對性能要求比較高的庫,並與 JavaScript 編寫的業務邏輯進行通訊,並在 JavaScript 端對 DOM 節點進行操作。以筆者最近開發的白鷺引擎 5.0 的渲染庫為例,白鷺引擎對外提供 JavaScript API,開發者編寫的 JavaScript 邏輯代碼會匯總為一組命令隊列發送給 WebAssembly 層,然後 WebAssembly 負責所有的計算工作,最終生成一組基於 WebGL 格式的數據流,最後 JavaScript 對這組數據流進行簡單的解析並直接調用 DOM 的 WebGL 接口傳遞數據。
在實踐過程中,我們總結出 WebAssembly 的幾個不容易註意的優勢和缺點:
-
代碼體積很小,我們將大約 300k 左右(壓縮後)JavaScript 邏輯改用 WebAssembly 重寫後,體積僅有 90k 左右。雖然使用 WebAssembly 需要引入一個 50k-100k 的 JavaScript 類庫作為基礎設施,但是總體來看資源尺寸的優勢還是很大的。
-
由於代碼格式是二進制、無法直接在瀏覽器中看到源碼,盡管理論上仍然可以通過逆向工程一定程度上得到原有的業務邏輯,但是由於開發者可以在編譯時使用了 -O3 等激進的優化策略,所以最終反編譯得到的業務邏輯也是很難閱讀的。雖然理論上一切在客戶端的內容都是不安全的,但是與所有代碼都直接暴露給用戶相比,代碼安全性得到了很大的改善。
-
在運行 benchmark 等極限測試時,遊戲引擎使用 WebAssembly 並不比 JavaScript 有幾何量級的提升。筆者的推論是:由於 JavaScript 引擎的 JIT 機制會把經常運行的函數進行極限的編譯優化,所以在 benchmark 這種代碼大量反復執行的測試環境下,無論是 JavaScript 版本,還是 WebAssembly 版本,運行的都是高度優化後的機器碼,雖然 WebAssembly 版本仍然比 JavaScript 版有一定的性能優勢,但是並不明顯。
-
在運行業務邏輯代碼時,由於大部分業務邏輯代碼只運行一次,所以 JavaScript 引擎只會對這部分代碼進行簡單的編譯優化而非極限優化,所以運行這一部分代碼 WebAssembly 相比 JavaScript 版本而言提升巨大,但是因為上文所述,不建議開發者在編寫業務邏輯時使用 WebAssembly,所以這裏陷入了一個兩難。在目前而言,理想情況是除了底層庫之外,部分關鍵的涉及性能問題的邏輯也可以使用 WebAssembly 進行編寫。
綜上所述,目前為止由於 WebAssembly 還不是非常完善,所以它目前的主要作用是作為 JavaScript 生態的有益補充,與 JavaScript 共存而不是取而代之。但是通過其路線圖我們可以得知,WebAssembly 的設計思想非常優秀,目前所有存在的問題從長遠的角度來說都是可以解決的問題。在加上 WebAssembly 是非常罕見的由四大瀏覽器廠商共同宣布會大力支持並實現的功能,其瀏覽器兼容性問題也終究可以得到解決,再退一步,哪怕舊式瀏覽器不支持,由於 WebAssembly 支持回退到 JavaScript,也可以保證正常運行。
在目前階段,WebAssembly 適合大量密集計算、並且無需頻繁與 JavaScript 及 DOM 進行數據通訊的場景。比如遊戲渲染引擎、物理引擎、圖像音頻視頻處理編輯、加密算法等。
筆者認為,WebAssembly 就像當初的 HTML5 標準一樣,在公布之後最開始不被很多人看好,認為會有瀏覽器兼容性問題、各大瀏覽器廠商的實現問題、性能問題、用戶需求與用戶體驗問題,但在近年來 HTML5 終於得到了廣泛的使用,甚至有些人認為他可以在很多場景下取代 NativeApp ,而非僅僅是當年“取代 Flash”這一小目標。憑借著底層技術的跨越式發展,以及瀏覽器廠商的一致支持,WebAssembly 一定會有一個光明的未來,也許真的可以成為一顆 Web 開發的“銀色子彈”。
轉自:http://www.infoq.com/cn/news/2017/07/WebAssembly-solve-JavaScript
WebAssembly是解決JavaScript 痼疾的銀彈?