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