這是專門探索 JavaScript 及其所構(gòu)建的組件的系列文章的第6篇。
推薦免費(fèi):JavaScript
這次將講解 WebAssembly 是如何工作的,更重要的是,它是如何在性能方面與JavaScript進(jìn)行比較的:加載時(shí)間、執(zhí)行速度、垃圾收集、內(nèi)存使用、API開放平臺(tái)、調(diào)試、多線程和可移植性。
首先,讓我們看看WebAssembly做什么
首先,我們有必要了解一下asm.js。2012年,Mozilla 的工程師 Alon Zakai 在研究 LLVM 編譯器時(shí)突發(fā)奇想:許多 3D 游戲都是用 C / C++ 語言寫的,如果能將 C / C++ 語言編譯成 JavaScript 代碼,它們不就能在瀏覽器里運(yùn)行了嗎?眾所周知,JavaScript 的基本語法與 C 語言高度相似。于是,他開始研究怎么才能實(shí)現(xiàn)這個(gè)目標(biāo),為此專門做了一個(gè)編譯器項(xiàng)目 Emscripten。這個(gè)編譯器可以將 C / C++ 代碼編譯成 JS 代碼,但不是普通的 JS,而是一種叫做 asm.js 的 JavaScript 變體,性能差不多是原生代碼的50%。
之后Google開發(fā)了Portable Native Client,也是一種能讓瀏覽器運(yùn)行C/C++代碼的技術(shù)。 后來可能是因?yàn)楸舜酥g有共同的更高追求,Google, Microsoft, Mozilla, Apple等幾家大公司一起合作開發(fā)了一個(gè)面向Web的通用二進(jìn)制和文本格式的項(xiàng)目,那就是WebAssembly。asm.js 與 WebAssembly 功能基本一致,就是轉(zhuǎn)出來的代碼不一樣:asm.js 是文本,WebAssembly 是二進(jìn)制字節(jié)碼,因此運(yùn)行速度更快、體積更小。
WebAssembly(又稱 wasm) 是一種新的字節(jié)碼格式,主流瀏覽器都已經(jīng)支持 WebAssembly。 和 JS 需要解釋執(zhí)行不同的是,WebAssembly 字節(jié)碼和底層機(jī)器碼很相似可快速裝載運(yùn)行,因此性能相對(duì)于 JS 解釋執(zhí)行大大提升。 也就是說 WebAssembly 并不是一門編程語言,而是一份字節(jié)碼標(biāo)準(zhǔn),需要用高級(jí)編程語言編譯出字節(jié)碼放到 WebAssembly 虛擬機(jī)中才能運(yùn)行, 瀏覽器廠商需要做的就是根據(jù) WebAssembly 規(guī)范實(shí)現(xiàn)虛擬機(jī)。
WebAssembly 加載時(shí)間
WebAssembly 在瀏覽器中加載速度更快,因?yàn)橹挥幸呀?jīng)編譯好的 wasm 文件需要通過internet傳輸。wasm 是一種低級(jí)匯編語言,具有非常簡(jiǎn)潔的二進(jìn)制格式。
WebAssembly 執(zhí)行速度
如今 Wasm 運(yùn)行速度只比原生代碼慢 20%,這是一個(gè)令人驚喜的結(jié)果。它是這樣的一種格式,會(huì)被編譯進(jìn)沙箱環(huán)境中且在大量的約束條件下運(yùn)行以保證沒有任何安全漏洞或者使之強(qiáng)化。和真正的原生代碼比較,執(zhí)行速度的下降微乎其微。更重要的是,未來將會(huì)更加快速。
更好的是,它與瀏覽器無關(guān)——所有主要引擎都增加了對(duì) WebAssembly的支持,且執(zhí)行速度相差無幾。
為了理解與JavaScript相比WebAssembly的執(zhí)行速度有多快,應(yīng)該首先閱讀關(guān)于JavaScript引擎如何工作的文章。
讓我們快速瀏覽下 V8 的運(yùn)行機(jī)制:
在左邊,是一些JavaScript源代碼,包含JavaScript函數(shù)。首先需要解析它,以便將所有字符串轉(zhuǎn)換為標(biāo)記并生成抽象語法樹(AST)。AST 是JavaScript程序邏輯結(jié)構(gòu)在內(nèi)存中的表示形式。一旦生成了 AST,V8 直接進(jìn)入到機(jī)器碼階段。其后遍歷樹,生成機(jī)器碼,就得到了編譯好的函數(shù),在這個(gè)過程中是沒有提高遍歷速度的。
現(xiàn)在,讓我們看看V8管道在下一階段的工作:
現(xiàn)在有了V8 的新的優(yōu)化編譯器 (TurboFan), 當(dāng) JavaScript應(yīng)用程序在運(yùn)行時(shí),很多代碼都在 V8 中運(yùn)行。TurboFan 監(jiān)測(cè)是否有代碼運(yùn)行緩慢,是否存在性能瓶頸和熱點(diǎn)(內(nèi)存使用過高的地方),以便對(duì)其進(jìn)行優(yōu)化。它把以上監(jiān)視得到的代碼推向后端即優(yōu)化過的即時(shí)編譯器,該編譯器把消耗大量 CPU 資源的函數(shù)轉(zhuǎn)換為性能更優(yōu)的代碼。
它解決了性能的問題,但這種處理方式有個(gè)缺點(diǎn),分析代碼和決定優(yōu)化哪些內(nèi)容的過程也會(huì)消耗CPU,這意味著更高的耗電量,特別是在移動(dòng)設(shè)備上。
但是,wasm 并不需要以上的全部步驟-如下所示是它被插入到執(zhí)行過程示意圖:
在編譯階段,WebAssembly 不需要被轉(zhuǎn)換,因?yàn)樗呀?jīng)是字節(jié)碼了。總之,以上的解析不在需要,你擁有優(yōu)化后的二進(jìn)制代碼可以直接插入到后端(即時(shí)編譯器)并生成機(jī)器碼。編譯器在前端已經(jīng)完成了所有的代碼優(yōu)化工作。
由于跳過了編譯過程中的不少步驟,這使得 wasm 的執(zhí)行更加高效。
WebAssembly 內(nèi)存模型
例如,編譯 成WebAssembly 的c++ 程序的內(nèi)存是一個(gè)連續(xù)的內(nèi)存塊,其中沒有“漏洞”。wasm 有助于提高安全性的一個(gè)特性是執(zhí)行堆棧與線性內(nèi)存分離的概念。在 c++ 程序中,如果有一個(gè)堆,從堆的底部進(jìn)行分配,然后從其頂部獲得內(nèi)存來增加內(nèi)存堆棧的大小。你可以獲得一個(gè)指針然后在堆棧內(nèi)存中遍歷以操作你不應(yīng)該接觸到的變量。
這是大多數(shù)可疑軟件可以利用的漏洞。
WebAssembly采用了完全不同的內(nèi)在模式。執(zhí)行堆棧與 WebAssembly 程序本身是分開的,因此無法在其中修改和更改諸如變量的值。同樣,這些函數(shù)使用整數(shù)偏移量,而不是指針。函數(shù)指向一個(gè)間接函數(shù)表。之后,這些直接的計(jì)算出的數(shù)字進(jìn)入模塊中的函數(shù)。通過這種方式構(gòu)建的,可以同時(shí)加載多個(gè) wasm 模塊,偏移所有索引且每個(gè)模塊都運(yùn)行良好。