WebAssembly,Web的新時代
文章推薦指數: 80 %
在瀏覽器之爭中,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
代碼1表示帶有兩個參數(a和b)的JavaScript函數,和通常JavaScript代碼不同的地方在於a=a | 0及b=b | 0,以及返回值後面均利用標註進行了按位OR操作。
這麼做的優點是使JavaScript引擎強制轉換變量的值為整型執行。
通過標註加上變量類型,JavaScript引擎就能更快地編譯。
既然增加變量類型能夠提升Web性能,有沒有辦法將靜態類型代碼例如C/C++等轉換成JavaScript指令的子集呢?上面的這段代碼恰恰是作為JavaScript子集的asm.js,由代碼2的C語言編譯而來:
代碼2
事實上,早在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所示。
代碼3
上述代碼轉換為WebAssembly的文本格式稍顯複雜,為了理解方便,我們從精簡的asm.js開始(見代碼4)。
代碼4
wast文本文件
將asm.js代碼轉換為WebAssembly的文本格式 add.wast(轉換工具見本文工具鏈章節,如代碼5所示)。
代碼4
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二進位文件後,最終轉換到機器碼執行。
圖1 經過編譯的二進位文件
工具鏈
開發人員現在可以使用相應的工具鏈從C / C ++源文件編譯WebAssembly模塊。
WebAssembly由許多工具支持,以幫助開發人員構建和處理源文件和生成的二進位內容。
Emscripten
Emscripten是其中無法迴避的工具之一,如圖2所示。
在圖2中,Emscripten SDK管理器(emsdk)用於管理多個SDK和工具,並且指定當前正被使用到編譯代碼的特定SDK和工具集。
圖2 Emscripten工具鏈流程圖及生成JavaScript(asm.js)流程
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容易、快速、有效。
它包含且不僅僅包含下面的幾個工具。
圖3 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所示)。
圖4 Emscripten+Binaryen生成WebAssembly的完整流程
由此可見,Emscripten以及Binaryen提供了完整的C/C++到WebAssembly的解決方案。
而Binaryen則幫助提升了WebAssembly的工具鏈生態。
提示
由於WebAssembly正處於活躍開發階段,各項編譯步驟和編譯工具會有大幅變更和改進,相信最終的編譯工具和步驟會趨於便捷,開發者需要留意官方網站的最新動態。
實戰
Linux和mac OS平台編譯原生代碼到WebAssembly可由如下步驟實現。
編譯環境準備
作業系統必須有可以工作的編譯器工具鏈,因此需要安裝GCC、cmake環境,此外Python、Node.js及Java環境也是需要的(其中Java為可選,如圖5所示)。
圖5 編譯環境安裝
如果是以其他方式安裝了Node.js,可能需要更新~/.emscripten文件的NODE_JS屬性。
安裝正確的emscripten分支
要編譯原生代碼到WebAssembly,我們需要emscripten的incoming分支。
由於emscripten不僅僅是用於WebAssembly的編譯工具鏈,選擇正確的分支尤為重要(如圖6所示)。
圖6 安裝emscripten的incoming分支
處理安裝異常
可運行emcc -v命令進行驗證安裝。
如果遇到如圖7所示的錯誤,表明帶有JavaScript後端的LLVM編譯器並未被生成。
圖7 emcc -v命令報錯
圖8 emcc -v命令報錯解決方案
通過圖8步驟,可以解決該問題,並且在~/.emscripten 文件中修改如下配置:
開始編譯程序
現在一個完整的工具鏈已經具備,我們可以使用它來編譯簡單的程序到WebAssembly。
但是,還有一些其他注意事項:
-
必須通過參數-s Wasm=1到emcc(否則默認emcc將編譯出asm.js);
-
除了Wasm二進位文件和JavaScript wrapper外,如果還希望emscripten生成一個可直接運行的程序的HTML頁面,則必須指定一個擴展名為.html的輸出文件。
在編譯之前,首先準備一個最基本的add.c程序,見代碼6。
代碼6
按代碼7所示的命令編輯好add.c程序並編譯:
運行WebAssembly應用
以Chrome瀏覽器為例,如果直接在瀏覽器內本地打開HTML文件,會有圖9所示的錯誤:
圖9 XMLHttpRequest本地訪問的跨域請求錯誤
由於XMLHttpRequest跨域請求不支持file://協議,必須經由HTTP實際輸出,可以由Python的SimplHTTPServer改進,見代碼8:
代碼8
在瀏覽器中輸入http://127.0.0.1:8080並打開add.html,就能直接看到轉換成WebAssembly的應用程式輸出結果。
創建獨立WebAssembly
默認情況下,emcc會創建JavaScript文件和WebAssembly的組合,其中JS加載包含編譯代碼的WebAssembly。
對於C/C++開發人員,他們可能更傾向於創建獨立的WebAssembly,用於JavaScript開發人員調用,見代碼9。
代碼9
上述命令運行後,我們可以得到獨立的Wasm文件。
需要說明的是,該參數仍然在開發中,可能隨時發生規範和實現變更。
JavaScript API調用
從C/C++程序編譯獲得一個.wasm模塊之後,JavaScript開發人員可以通過如下方式進行載入.wasm文件並執行。
WebAssembly社區組也有計劃通過Streams使用streaming以及異步編譯,見代碼10。
代碼10
最後一行調用導出的WebAssembly函數,它反過來調用我們導入的JS函數,最終執行add(201700, 2),並且在控制台獲得期望的結果輸出(如圖10所示)。
圖10 WebAssembly求和函數在控制台的輸出
性能
那麼,WebAssembly的真實性能如何呢?首先我們用一直被用來作為CPU基準測試的斐波那契 (Fibonacci)數列來進行對比,這裡使用的是性能較差的遞歸算法,在Node.js v7.2.1環境下,能夠看到WebAssembly性能優勢越發明顯(如圖11所示)。
圖11 CPU基準測試反應WebAssembly的真實性能
再看看最基本的1000毫秒時間內,求和計算的運算量統計,在同一台計算機的Firefox 50.1.0版本的運算結果如圖12所示。
圖12 1000毫秒內求和計算的運算量統計
儘管重複測試時結果不盡相同,重啟瀏覽器並多次測試取平均值後依然可以看到WebAssembly的運算量比JavaScript快了近一個量級。
Demo
圖13展示了Angry Bots Demo,它是由WebAssembly項目發布的一個Demo,由Unity遊戲移植而來。
圖13 Angry Bots Demo / Google Chrome 55.0.2883.87
通過如下方式可以體驗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所示)。
圖14 WebAssembly與JavaScript引擎的關係
WebAssembly被設計為對JavaScript的補充,而不是替代,是為了提供一種方法來獲得應用程式的關鍵部分接近原生性能。
隨著時間的推移,雖然WebAssembly將允許多種語言(不僅僅是C/C++)被編譯到Web,但是JavaScript的發展勢頭不會因此被削弱,並且仍然將保持Web的單一動態語言。
此外,由於WebAssembly構建在JavaScript引擎的基礎架構上,JavaScript和WebAssembly將在許多場景中配合使用。
那麼WebAssembly是不是僅僅面向C/C++開發者呢?答案依舊是否定的。
WebAssembly最初實現的重點是C/C++,由Mozilla主導開發的注重高效、安全和並行的Rust也能在2016年末被成功編譯到WebAssembly了,未來還會繼續增加其他語言的支持,見代碼11。
代碼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的新時代。
作者簡介
張敏,Intel開源技術中心Web團隊軟體技術經理,原Opera Software軟體經理,在瀏覽器及Web Runtime領域工作10年,專注於Web及開源技術。
責編:陳秋歌,歡迎前端開發領域技術投稿、約稿、給文章糾錯,請發送郵件至[email protected]
帶你從底層詳細剖析Android性能落後於iOS之語言篇
一直以來Android性能一直落後於iOS,即使在Android系統的手機配置高於蘋果手機許多的情況下仍然處於下風,本文帶你從開發語言角度解釋其中的重要原因,大家都知道Android系統的應用程...