WebAssembly,Web的新時代
在瀏覽器之爭中,Chrome憑藉JavaScript的卓越效能取得了市場主導地位,然而由於JavaScript的無型別特性,導致其執行時消耗大量的效能做為代價,這也是JavaScript的瓶頸之一。WebAssembly旨在解決這一問題。本文從WebAssembly的起源到開發實踐對其做全面探究,幫助開發者對WebAssembly有全面的瞭解。
緣起
讓我們從瀏覽器大戰說起。微軟憑藉Windows系統捆綁Internet Explorer的先天優勢擊潰Netscape後,進入了長達數年的靜默期。而Netscape則於1998年將Communicator開源,並由Mozilla基金會衍生出Firefox瀏覽器,在2004年釋出了1.0版本。從此,第二次瀏覽器大戰拉開帷幕。這場大戰由Firefox瀏覽器領銜,Safari、Opera等瀏覽器也積極進取,Internet Explorer的主導地位首次受到挑戰。2008年Google推出Chrome瀏覽器,不但逐步侵蝕Firefox的市場,更是壓制了老邁的Internet Explorer。在此次大戰之後的2012年,StatCounter的資料指出Chrome以微弱優勢超越Internet Explorer成為世界上最流行的瀏覽器。
分析Google Chrome瀏覽器戰勝Internet Explorer的原因,除了對Web標準更友善的支援外,卓越的效能是其中相當重要的因素,而瀏覽器效能之爭的本質則體現在JavaScript引擎。此前,JavaScript引擎的實現方式經歷了遍歷語法樹到位元組碼直譯器等較為原始的方式,將每條原始碼翻譯成相應的機器碼並執行,並不儲存翻譯後的機器碼,使得解釋執行很慢。2008年9月,Google釋出了V8 JavaScript引擎。V8被設計用於提高Web瀏覽器中JavaScript的執行效能,通過即時編譯JIT(Just-In-Time)技術,在執行時將JavaScript程式碼編譯成更為高效的機器程式碼並儲存,下次執行同一程式碼段時無需再編譯,使得JavaScript獲得了幾十倍的效能提升。
然而,JavaScript是個無型別(untyped,變數沒有型別)的語言,這直接導致表示式c=a+b有多重含義:
- a、b均為數字,則算術運算子+表示值相加;
- a、b為字串,則+運算子表示字串連線;
- …
表示式執行時,JIT編譯器需要檢查a和b的型別,確定操作行為。若a、b均為數字,JIT編譯器則將a、b確認為整型,而一旦某一變數變成字串,JIT編譯器則不得不將之前編譯的機器碼推倒重來。由此可見,JavaScript的無型別特性建立在消耗大量效能代價的基礎之上。即便JIT編譯器在對變數型別發生變化時已進行相應優化,但仍然有很多情況JavaScript引擎未進行或無法優化,例如for-of、try-catch、try-finally、with語句以及複合let、const賦值的函式等。
由此可見,JavaScript的無型別是JavaScript引擎的效能瓶頸之一,改進方案有兩種:一是設計一門新的強型別語言並強制開發者進行型別指定;二是給現有的JavaScript加上變數型別。
微軟開發的TypeScript屬於第一種改進方案。它是擴充套件了JavaScript特性的語言,包含了型別批註,編譯時型別檢查,型別推斷和擦除等功能,TypeScript開發者在宣告變數時指定型別,使得JavaScript引擎能夠更快將這種強型別的語言編譯成弱型別。
看看第二種方案:
程式碼1表示帶有兩個引數(a和b)的JavaScript函式,和通常JavaScript程式碼不同的地方在於a=a | 0及b=b | 0,以及返回值後面均利用標註進行了按位OR操作。這麼做的優點是使JavaScript引擎強制轉換變數的值為整型執行。通過標註加上變數型別,JavaScript引擎就能更快地編譯。
既然增加變數型別能夠提升Web效能,有沒有辦法將靜態型別程式碼例如C/C++等轉換成JavaScript指令的子集呢?上面的這段程式碼恰恰是作為JavaScript子集的asm.js,由程式碼2的C語言編譯而來:
事實上,早在1995年起就已經有Netscape Plugin API(NPAPI)在內的可以使用瀏覽器執行C/C++程式的專案在開發。而2013年問世的asm.js是目前較為廣泛的方案。asm.js是一種中間程式語言,允許用C/C++語言編寫的計算機軟體作為Web應用程式執行,並保持更好的效能,而Mozilla Firefox從版本22起成為第一個為asm.js特別優化的網頁瀏覽器。
Google也同樣在為原生程式碼執行在Web端而努力。Google Native Client(NaCl)採用沙盒技術,讓Intel x86、ARM或MIPS子集的機器碼直接在沙盒上執行。它能夠在無需安裝外掛的情況下從瀏覽器直接執行原生可執行程式碼,使Web應用程式可以用接近於機器碼運作的速度來執行。而Google Portable Native Client(PNaCl)則稍有變化,通過一些前端編譯器將C/C++原始碼編譯成LLVM的中間位元組碼而不是x86或ARM程式碼,並且進行優化以及連結(如表1所示)。
有了型別支援,第二種方案效能提升潛力遠遠大於第一種。
然而,無論是asm.js或現有PNaCl的解決方案,都面臨著一些缺陷(例如1KB的C原始碼編譯生成asm.js後的大小有480KB)或其他瀏覽器不支援的窘境,而2016年10月對Chromium問題跟蹤程式碼的評論更是表明,Google Native Client小組已被關閉。
作為Web瀏覽器效能和程式碼重用的解決方案,asm.js及PNaCl都沒能被普遍接受,那麼有沒有上述表格中的特性全部佔優,且跨廠商的解決方案呢?
WebAssembly旨在解決這個問題。
新時代
WebAssembly(簡稱Wasm)是一種新的適合於編譯到Web的,可移植的,大小和載入時間高效的格式。這是一個新的與平臺無關的二進位制程式碼格式,目標是解決JavaScript效能問題。這個新的二進位制格式遠小於JavaScript,可由瀏覽器的JavaScript引擎直接載入和執行,這樣可節省從JavaScript到位元組碼,從位元組碼到執行前的機器碼所花費的即時編譯JIT(Just-In-Time)時間。 作為一種低階語言,它定義了一個抽象語法樹(Abstract Syntax Tree,AST),開發人員可以以文字格式進行除錯。
WebAssembly描述了一個記憶體安全的沙箱執行環境,可以在現有的JavaScript虛擬機器中實現。 當嵌入到Web中時,WebAssembly將強制執行瀏覽器的同源和許可權安全策略。因此,和經常出現安全漏洞的Flash外掛相比,WebAssembly是一個更加安全的解決方案。
WebAssembly可由C/C++等語言編譯而來。此外,WebAssembly由Google、Mozilla、微軟以及蘋果公司牽頭的W3C社群組共同努力,基本覆蓋主流的瀏覽器廠商,因此其可移植性相較Silverlight等有極大提升,平臺相容問題將不復出現。
在Web平臺的很多專案中,對於原生新功能的支援需要Web瀏覽器或Runtime提供複雜的標準化的API來實現,但是JavaScript API往往較慢。使用WebAssembly,這些標準API可以更簡單,並且操作在更低的水平。例如,對於一個面部識別的Web專案,對於訪問資料流我們可以由簡單的JavaScript API實現,而把面部識別原生SDK做的事情交由WebAssembly實現。
需要了解的是,WebAssembly不是將C/C++等其他語言編譯到JavaScript,更不是一種新的程式語言。
探究
asm.js
上文的C語言求和程式碼經由編譯器生成asm.js後如程式碼3所示。
上述程式碼轉換為WebAssembly的文字格式稍顯複雜,為了理解方便,我們從精簡的asm.js開始(見程式碼4)。
wast文字檔案
將asm.js程式碼轉換為WebAssembly的文字格式 add.wast(轉換工具見本文工具鏈章節,如程式碼5所示)。
WebAssembly中程式碼的可裝載和可執行單元被稱為一個模組(module)。在執行時,一個模組可以被一組import值例項化,多個模組例項能夠訪問相同的共享狀態。目前文字格式中的module主要用S表示式來表示。雖然S表達格式不是正式的文字格式,但它易於表示AST。WebAssembly也被設計為與ES6的modules整合。
一個單一的邏輯函式定義包含兩個部分:功能部分宣告在模組中每個內部函式定義的簽名,程式碼段部分包含由功能部分宣告的每個函式的函式體。WebAssembly是帶有返回值的靜態型別,並且所有引數都含有型別。上面的add.wast可以解讀為:
- 聲明瞭一個名為$add的函式;
- 包含兩個引數a和b,兩者都是32位整型;
- 結果是一個32位整型;
- 函式體是一個32位的加法:
- 上面是區域性變數$a得到的值;
- 下面是區域性變數$b得到的值;
- 由於沒有明確的返回節點,因此return是該加法函式的最後載入指令。
二進位制Wasm檔案
如圖1所示,由C語言求和程式碼經過編譯生成二進位制檔案,通讀檔案可以找到相應的頭部、型別、匯入、函式以及程式碼段等。通過JavaScript API載入Wasm二進位制檔案後,最終轉換到機器碼執行。
工具鏈
開發人員現在可以使用相應的工具鏈從C / C ++原始檔編譯WebAssembly模組。WebAssembly由許多工具支援,以幫助開發人員構建和處理原始檔和生成的二進位制內容。
Emscripten
Emscripten是其中無法迴避的工具之一,如圖2所示。在圖2中,Emscripten SDK管理器(emsdk)用於管理多個SDK和工具,並且指定當前正被使用到編譯程式碼的特定SDK和工具集。
Emscripten的主要工具是Emscripten編譯器前端(emcc),它是例如GCC的標準編譯器的簡易替代實現。
Emcc使用Clang將C/C++檔案轉換為LLVM(源自於底層虛擬機器Low Level Virtual Machine)位元組碼,使用Fastcomp(Emscripten的編譯器核心,一個LLVM後端)把位元組碼編譯成JavaScript。輸出的JavaScript可以由Node.js執行,或者嵌入HTML在瀏覽器中執行。這帶來的直接結果就是,C和C++程式經過編譯後可在JavaScript上執行,無需任何外掛。
WABT和Binaryen
除此之外,對於想要使用由其他工具(如Emscripten)生成的WebAssembly二進位制檔案感興趣的開發者,目前http://webassembly.org/官方額外提供了另外兩組不同的工具:
- WABT ——WebAssembly二進位制工具包;
- Binaryen——編譯器和工具鏈。
WABT工具包支援將二進位制WebAssembly格式轉換為可讀的文字格式。其中wasm2wast命令列工具可以將WebAssembly二進位制檔案轉換為可讀的S表示式文字檔案。而wast2wasm命令列工具則執行完全相反的過程。
Binaryen則是一套更為全面的工具鏈,是用C++編寫成用於WebAssembly的編譯器和工具鏈基礎結構庫(如圖3所示)。WebAssembly是二進位制格式(Binary Format)並且和Emscripten整合,因此該工具以Binary和Emscript-en的末尾合併命名為Binaryen。它旨在使編譯WebAssembly容易、快速、有效。它包含且不僅僅包含下面的幾個工具。
- wasm-as:將WebAssembly由文字格式(當前為S表示式格式)編譯成二進位制格式;
- wasm-dis:將二進位制格式的WebAssembly反編譯成文字格式;
- asm2wasm:將asm.js編譯到WebAssembly文字格式,使用Emscripten的asm優化器;
- s2wasm:在LLVM中開發,由新WebAssembly後端產生的.s格式的編譯器;
- wasm.js:包含編譯為JavaScript的Binaryen元件,包括直譯器、asm2wasm、S表示式解析器等。
Binaryen目前提供了兩個生成WebAssembly的流程,由於emscripten的asm.js生成已經非常穩定,並且asm2wasm是一個相當簡單的過程,所以這種將C/C ++編譯為WebAssembly的方法已經可用(如圖4所示)。
由此可見,Emscripten以及Binaryen提供了完整的C/C++到WebAssembly的解決方案。而Binaryen則幫助提升了WebAssembly的工具鏈生態。
提示
由於WebAssembly正處於活躍開發階段,各項編譯步驟和編譯工具會有大幅變更和改進,相信最終的編譯工具和步驟會趨於便捷,開發者需要留意官方網站的最新動態。
實戰
Linux和mac OS平臺編譯原生程式碼到WebAssembly可由如下步驟實現。
編譯環境準備
作業系統必須有可以工作的編譯器工具鏈,因此需要安裝GCC、cmake環境,此外Python、Node.js及Java環境也是需要的(其中Java為可選,如圖5所示)。
如果是以其他方式安裝了Node.js,可能需要更新~/.emscripten檔案的NODE_JS屬性。
安裝正確的emscripten分支
要編譯原生程式碼到WebAssembly,我們需要emscripten的incoming分支。由於emscripten不僅僅是用於WebAssembly的編譯工具鏈,選擇正確的分支尤為重要(如圖6所示)。
處理安裝異常
可執行emcc -v命令進行驗證安裝。如果遇到如圖7所示的錯誤,表明帶有JavaScript後端的LLVM編譯器並未被生成。
通過圖8步驟,可以解決該問題,並且在~/.emscripten 檔案中修改如下配置:
開始編譯程式
現在一個完整的工具鏈已經具備,我們可以使用它來編譯簡單的程式到WebAssembly。但是,還有一些其他注意事項:
- 必須通過引數-s Wasm=1到emcc(否則預設emcc將編譯出asm.js);
- 除了Wasm二進位制檔案和JavaScript wrapper外,如果還希望emscripten生成一個可直接執行的程式的HTML頁面,則必須指定一個副檔名為.html的輸出檔案。
在編譯之前,首先準備一個最基本的add.c程式,見程式碼6。
按程式碼7所示的命令編輯好add.c程式並編譯:
執行WebAssembly應用
以Chrome瀏覽器為例,如果直接在瀏覽器內本地開啟HTML檔案,會有圖9所示的錯誤:
由於XMLHttpRequest跨域請求不支援file://協議,必須經由HTTP實際輸出,可以由Python的SimplHTTPServer改進,見程式碼8:
在瀏覽器中輸入http://127.0.0.1:8080並開啟add.html,就能直接看到轉換成WebAssembly的應用程式輸出結果。
建立獨立WebAssembly
預設情況下,emcc會建立JavaScript檔案和WebAssembly的組合,其中JS載入包含編譯程式碼的WebAssembly。對於C/C++開發人員,他們可能更傾向於建立獨立的WebAssembly,用於JavaScript開發人員呼叫,見程式碼9。
上述命令執行後,我們可以得到獨立的Wasm檔案。需要說明的是,該引數仍然在開發中,可能隨時發生規範和實現變更。
JavaScript API呼叫
從C/C++程式編譯獲得一個.wasm模組之後,JavaScript開發人員可以通過如下方式進行載入.wasm檔案並執行。WebAssembly社群組也有計劃通過Streams使用streaming以及非同步編譯,見程式碼10。
最後一行呼叫匯出的WebAssembly函式,它反過來呼叫我們匯入的JS函式,最終執行add(201700, 2),並且在控制檯獲得期望的結果輸出(如圖10所示)。
效能
那麼,WebAssembly的真實效能如何呢?首先我們用一直被用來作為CPU基準測試的斐波那契 (Fibonacci)數列來進行對比,這裡使用的是效能較差的遞迴演算法,在Node.js v7.2.1環境下,能夠看到WebAssembly效能優勢越發明顯(如圖11所示)。
再看看最基本的1000毫秒時間內,求和計算的運算量統計,在同一臺計算機的Firefox 50.1.0版本的運算結果如圖12所示。
儘管重複測試時結果不盡相同,重啟瀏覽器並多次測試取平均值後依然可以看到WebAssembly的運算量比JavaScript快了近一個量級。
Demo
圖13展示了Angry Bots Demo,它是由WebAssembly專案釋出的一個Demo,由Unity遊戲移植而來。
通過如下方式可以體驗WebAssembly在瀏覽器中的強大效能。即便Google Chrome較新的穩定版也已支援WebAssembly,還是推薦使用canary版及Firefox的nightly版進行測試。
- 下載瀏覽器:
1-1. Google Chrome;
1-2. Mozilla Firefox;
1-3. Opera;
1-4. Vivaldi。 - 開啟 WebAssembly支援 :
2-1. Google Chrome:chrome://flags/#enable-webassembly;
2-2. Mozilla Firefox:about:config→接受→搜尋javascript.options.wasm→設定為true;
2-3. Opera:opera://flags/#enable-webassembly;
2-4. Vivaldi:vivaldi://flags#enable-webassembly。
使用W、A、S、D等鍵實現移動操作,點選滑鼠進行射擊。該WebAssembly遊戲在瀏覽器中執行相當流暢,媲美原生效能。
除了最新的瀏覽器開始對WebAssembly逐步支援外,Intel開源技術中心開發的Crosswalk專案(https://crosswalk-project.org/)早在2016年11月初的Crosswalk 22穩定版(Windows及Android 平臺)即已加入對WebAssembly實驗性的支援,開發者可以使用該版本體驗Angry Bots Demo。
開發者
WebAssembly對於Web有顯著的效能提升,對於開發者尤其是前端或者JavaScript開發人員而言,並不意味著WebAssembly將會取代JavaScript(如圖14所示)。
WebAssembly被設計為對JavaScript的補充,而不是替代,是為了提供一種方法來獲得應用程式的關鍵部分接近原生效能。隨著時間的推移,雖然WebAssembly將允許多種語言(不僅僅是C/C++)被編譯到Web,但是JavaScript的發展勢頭不會因此被削弱,並且仍然將保持Web的單一動態語言。此外,由於WebAssembly構建在JavaScript引擎的基礎架構上,JavaScript和WebAssembly將在許多場景中配合使用。
那麼WebAssembly是不是僅僅面向C/C++開發者呢?答案依舊是否定的。WebAssembly最初實現的重點是C/C++,由Mozilla主導開發的注重高效、安全和並行的Rust也能在2016年末被成功編譯到WebAssembly了,未來還會繼續增加其他語言的支援,見程式碼11。
在未來,通過ES6模組介面與JavaScript整合,Web開發人員並不需要編寫C++,而是可以直接利用其他人編寫的庫,重用模組化C++庫可以像使用JavaScript中的modules一樣簡單。
進展
依據開發路線圖,2016年10月31日,WebAssembly到達瀏覽器預覽的里程碑。Google Chrome V8引擎及Mozilla Firefox SpiderMonkey引擎都已經在trunk上支援WebAssembly瀏覽器預覽。2016年12月下旬,Microsoft Edge瀏覽器使用的JavaScript引擎ChakraCore v1.4.0啟用了WebAssembly瀏覽器預覽支援。而Webkit JavaScriptCore引擎對於該支援也在積極進行中。
目前,WebAssembly社群組已經有初始(MVP)二進位制格式釋出候選和JavaScript API在多個瀏覽器中實現。作為瀏覽器預覽期間的一部分,WebAssembly社群組(WebAssembly Community Group)現在正在徵求更廣泛的社群反饋。社群組的初步目標是瀏覽器預覽在2017年第一季度結束,但在瀏覽器預覽期間的重大發現可能會延長該週期。當瀏覽器預覽結束時,社群組將產生WebAssembly的草案規範,並且瀏覽器廠商可以開始預設提供符合規範的實現。預計在2017年上半年,四大主流瀏覽器對原生的WebAssembly支援將到達穩定版。
具體到Google V8引擎的最新進展,asm.js程式碼將不再通過Turbofan JavaScript編譯器而是編譯到WebAssembly後,在WebAssembly的原生執行環境中執行最終的機器碼。這種改變帶來的好處有,為asm.js將預先編譯(AOT,Ahead Of Time Compilation)帶到了Chrome,且完全向後相容。新的WebAssembly編譯渠道重用了一些Turbofan JavaScript編譯器後端部分,因此能夠在少了很多編譯和優化消耗的前提下,產生類似的程式碼。在Google Chrome中,WebAssembly將很快在Canary版中預設啟用,開發團隊也期望能夠釋出到2017年第一季度末的穩定版中。
社群
包含所有主要瀏覽器廠商代表的W3C Web——Assembly社群組於2015年4月底成立。該小組的任務是,在編譯到適用於Web的新的、便攜的、大小和載入時間高效的格式上,促進早期的跨瀏覽器協作。該社群組也正在將WebAssembly設計為W3C開放標準。目前,除了文中所述主流瀏覽器廠商Mozilla、Google、微軟、及蘋果公司之外,Opera CTO及Intel的8位該領域專家均參與了該社群組。當然,並不是只有社群組成員才能參與標準的制定,任何人都可以在https://github.com/WebAssembly做出貢獻。
展望
由於主要的瀏覽器廠商對WebAssembly支援表現積極,並且都在實現WebAssembly的各項功能,因此在Web中高效能需求的應用例如線上遊戲、音樂、視訊流、AR/VR、平臺模擬、虛擬機器、遠端桌面、壓縮及加密等都能夠獲得接近於原生的效能。相信WebAssembly將會開創Web的新時代。