1、WebAssembly是什么?
WebAssembly 是一種低級的類匯編語言,具有緊湊的二進(jìn)制格式,可以接近原生的性能運(yùn)行。它設(shè)計(jì)的目的不是為了手寫代碼而是為諸如C、C++、Rust、AssemblyScript等低級源語言提供一個(gè)高效的編譯目標(biāo)。
2、WebAssembly有什么意義?
和 JS 需要解釋執(zhí)行不同的是,WebAssembly 字節(jié)碼和底層機(jī)器碼很相似可快速裝載運(yùn)行,因此性能相對于 JS 解釋執(zhí)行大大提升,為客戶端app提供了一種在網(wǎng)絡(luò)平臺以接近本地速度的方式運(yùn)行多種語言編寫的代碼的方式。解決3D游戲、虛擬現(xiàn)實(shí)、增強(qiáng)現(xiàn)實(shí)、計(jì)算機(jī)視覺、圖像/視頻編輯在瀏覽器上的性能問題。
WebAssembly的模塊可以被導(dǎo)入的到一個(gè)網(wǎng)絡(luò)app(或Node.js)中,并且暴露出供JavaScript使用的WebAssembly函數(shù)。性能更快、更快、更快!
3、為什么WebAssembly性能更快?
想要知道JS 引擎運(yùn)行程序花費(fèi)的時(shí)間。我們需要知道JavaScript 做了什么事情。有幾個(gè)階段:
Parsing- 源碼轉(zhuǎn)換成解釋器可以運(yùn)行的東西所用的時(shí)間。
Compiling + optimizing- 花費(fèi)在基礎(chǔ)編譯和優(yōu)化編譯上的時(shí)間。
Re-optimizing- 當(dāng)預(yù)先編譯優(yōu)化的代碼不能被優(yōu)化的情況下,JIT 將這些代碼重新優(yōu)化,如果不能重新優(yōu)化那么久丟給基礎(chǔ)編譯去做。這個(gè)過程叫做重新優(yōu)化。
Execution- 執(zhí)行代碼的過程
Garbage collection- 清理內(nèi)存的時(shí)間
需要注意的是:這些任務(wù)不會(huì)發(fā)生在離散塊或特定的序列中。相反,它們將被交叉執(zhí)行。比如正在做一些代碼解析時(shí),還執(zhí)行者一些其他的邏輯,有些代碼編譯完成后,引擎又做了一些解析,然后又執(zhí)行了一些邏輯,等等。
3.1 JavaScript和WebAssembly性能比較
3.1.1 請求
文件小:從服務(wù)器獲取文件是需要時(shí)間的,下載執(zhí)行與 JavaScript 等效的 WebAssembly 文件需要更少的時(shí)間,因?yàn)樗捏w積更小。WebAssembly 設(shè)計(jì)的體積更小,是一種二進(jìn)制形式。即使使用 gzip 壓縮的 JavaScript文件很小,但 WebAssembly 中的等效代碼可能更小。下載資源的時(shí)間會(huì)更少。
3.1.2 解析
字節(jié)碼:JavaScript 源碼一旦被下載到瀏覽器,源將被解析為抽象語法樹(AST)。在這個(gè)過程中,AST需要被轉(zhuǎn)換為該 JS 引擎所能識別的字節(jié)碼。
相反,WebAssembly不需要被轉(zhuǎn)換,因?yàn)樗呀?jīng)是字節(jié)碼了。它僅僅需要被解碼并確定沒有任何錯(cuò)誤。
3.1.3 編譯 + 優(yōu)化
JavaScript 是在執(zhí)行代碼期間編譯的。因?yàn)?JavaScript 是動(dòng)態(tài)類型語言,相同的代碼在多次執(zhí)行中都有可能都因?yàn)榇a里含有不同的類型數(shù)據(jù)被重新編譯。這樣會(huì)消耗時(shí)間。
相反,WebAssembly與機(jī)器代碼更接近。編譯器不需要在運(yùn)行代碼時(shí)花費(fèi)時(shí)間去觀察代碼中的數(shù)據(jù)類型,在開始編譯時(shí)做優(yōu)化。編譯器不需要去判斷每次執(zhí)行相同代碼中數(shù)據(jù)類型是否一樣。
3.1.4 重新優(yōu)化
無需重新優(yōu)化 :在 WebAssembly中,類型是明確的,不需要根據(jù)運(yùn)行時(shí)收集的數(shù)據(jù)對類型進(jìn)行假設(shè)。這意味著它不必經(jīng)過重新優(yōu)化的周期。
3.1.5 執(zhí)行
WebAssembly 提供了一組更適合機(jī)器的指令、執(zhí)行速度更快。
3.1.6 垃圾回收
在 JavaScript 中,開發(fā)者不需要擔(dān)心內(nèi)存中無用變量的回收。JS 引擎使用一個(gè)叫垃圾回收器的東西來自動(dòng)進(jìn)行垃圾回收處理。
這對于控制性能可能并不是一件好事。你并不能控制垃圾回收時(shí)機(jī),所以它可能在非常重要的時(shí)間去工作,從而影響性能。
現(xiàn)在,WebAssembly根本不支持垃圾回收。內(nèi)存是手動(dòng)管理的(就像 C/C++)。雖然這些可能讓開發(fā)者編程更困難,但它的確提升了性能。
總而言之,這些都是在許多情況下,在執(zhí)行相同任務(wù)時(shí)WebAssembly 將勝過 JavaScript 的原因。
3.1.7 兼容性問題
WebAssembly 是非常底層的字節(jié)碼規(guī)范,制訂好后很少變動(dòng),就算以后發(fā)生變化,也只需在從高級語言編譯成字節(jié)碼過程中做兼容。可能出現(xiàn)兼容性問題的地方在于 JS 和 WebAssembly 橋接的 JS 接口。
4、關(guān)鍵概念
有幾個(gè)關(guān)鍵概念需要注意的是:
模塊:表示一個(gè)已經(jīng)被瀏覽器編譯為可執(zhí)行機(jī)器碼的WebAssembly二進(jìn)制代碼。主要的特點(diǎn)是:二進(jìn)制對象(Blob)、無狀態(tài)、緩存(indexDB)、可以導(dǎo)入導(dǎo)出(但是目前還不支持ES6和script)。
內(nèi)存:ArrayBuffer、本質(zhì)上是連續(xù)的字節(jié)數(shù)組。
實(shí)例:一個(gè)模塊及其在運(yùn)行時(shí)使用的所有狀態(tài),包括內(nèi)存、表格和一系列導(dǎo)入值。一個(gè)實(shí)例就像一個(gè)已經(jīng)被加載到一個(gè)擁有一組特定導(dǎo)入的特定的全局變量的ES2015模塊。
5、具體如何實(shí)踐?
需要借助Emscripten編譯器(自行百度安裝),這個(gè)編譯器能夠?qū)⒁欢蜟/C++代碼,編譯出:
一個(gè).wasm模塊。
用來加載和運(yùn)行該模塊的JavaScript“膠水”代碼。
一個(gè)用來展示代碼運(yùn)行結(jié)果的HTML文檔。
簡而言之,工作流程如下所示:

Emscripten首先把C/C++提供給clang+LLVM——一個(gè)成熟的開源C/C++編譯器工具鏈,比如,在OSX上是XCode的一部分。
Emscripten將clang+LLVM編譯的結(jié)果轉(zhuǎn)換為一個(gè).wasm二進(jìn)制文件。
就自身而言,WebAssembly當(dāng)前不能直接的存取DOM;它只能調(diào)用JavaScript,并且只能傳入整形和浮點(diǎn)型的原始數(shù)據(jù)類型作為參數(shù)。這就是說,為了使用任何Web API,WebAssembly需要調(diào)用到JavaScript,然后由JavaScript調(diào)用Web API。因此,Emscripten創(chuàng)建了HTML和JavaScript膠水代碼以便完成這些功能。
5.1 JS調(diào)用WebAssembly
5.1.1 編譯
舉個(gè)例子:
有hello.c的這么一個(gè)文件。
#include
int main(int argc, char ** argv) {
printf("Hello World\n");
}
終端窗口中,進(jìn)入剛剛保存hello.c文件的文件夾中,然后運(yùn)行下列命令:
emcc hello.c -s WASM=1 -o hello.html
下面列出了我們命令中選項(xiàng)的細(xì)節(jié):
-s WASM=1 —指定我們想要的wasm輸出形式。如果我們不指定這個(gè)選項(xiàng),Emscripten默認(rèn)將只會(huì)生成asm.js。
-o hello.html —指定這個(gè)選項(xiàng)將會(huì)生成HTML頁面來運(yùn)行我們的代碼,并且會(huì)生成wasm模塊,以及編譯和實(shí)例化wasm模塊所需要的“膠水”js代碼。這個(gè)時(shí)候在您的源碼文件夾應(yīng)該有下列文件:
hello.wasm二進(jìn)制的wasm模塊代碼
hello.js一個(gè)包含了用來在原生C函數(shù)和JavaScript/wasm之間轉(zhuǎn)換的膠水代碼的JavaScript文件
hello.html一個(gè)用來加載,編譯,實(shí)例化你的wasm代碼并且將它輸出在瀏覽器顯示上的一個(gè)HTML文件。
注意:默認(rèn)情況下,Emscripten 生成的代碼只會(huì)調(diào)用 main() 函數(shù),其它的函數(shù)將被視為無用代碼。你需要導(dǎo)入 emscripten.h 庫來使用EMSCRIPTEN_KEEPALIVE宏,在一個(gè)函數(shù)名之前添加 EMSCRIPTEN_KEEPALIVE來使用這個(gè)宏。
5.1.2加載和運(yùn)行WebAssembly代碼
首先需要把模塊放入內(nèi)存。比如,通過XMLHttpRequest或Fetch,模塊將會(huì)被初始化為帶類型數(shù)組。
WebAssembly還沒有和<script type='module'>或ES6的import語句集成,也就是說,當(dāng)前還沒有內(nèi)置的方式讓瀏覽器為你獲取模塊。當(dāng)前唯一的方式就是創(chuàng)建一個(gè)包含你的WebAssembly模塊二進(jìn)制代碼的 ArrayBuffer 并且使WebAssembly.instantiate()編譯它。
Fetch獲取WebAssembly模塊:
該函數(shù)返回一個(gè)可以解析為Response對象的promise。
我們可以使用arrayBuffer()函數(shù)把響應(yīng)(response)轉(zhuǎn)換為帶類型數(shù)組,該函數(shù)返回一個(gè)可以解析為帶類型數(shù)組的promise。
最后,我們使用WebAssembly.instantiate()函數(shù)一步實(shí)現(xiàn)編譯和實(shí)例化帶類型數(shù)組。
代碼如下:
fetch('module.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results => {
//Do something with the compiled results!
});
5.2 C/C++調(diào)用JavaScript
Emscripten 允許 C / C++ 代碼直接調(diào)用JavaScript。
新建一個(gè)文件hello.c,寫入下面的代碼。
#include
int main() {
EM_ASM({ alert('Hello World!'); });
}
EM_ASM是一個(gè)宏,會(huì)調(diào)用嵌入的JavaScript 代碼。注意,JavaScript 代碼要寫在大括號里面。
然后,將這個(gè)程序編譯
$ emcc hello.c -o hello.html
瀏覽器打開hello.html,就會(huì)跳出對話框Hello World!。
5.3 C/C++ 與 JavaScript 的通信
Emscripten 允許 C / C++ 代碼與JavaScript 通信。
新建一個(gè)文件hello.c,寫入下面的代碼。
#include
int main() {
int val1 = 21;
int val2 = 2
int val = EM_ASM_INT({ return $0 * $1; }, val1, val2);
std::cout << "val == " << val<
}
上面代碼中,EM_ASM_INT表示 JavaScript 代碼返回的是一個(gè)整數(shù),它的參數(shù)里面的$0表示第一個(gè)參數(shù),$1表示第二個(gè)參數(shù),以此類推。EM_ASM_INT的其他參數(shù)會(huì)按照順序,傳入 JavaScript 表達(dá)式。
然后,將這個(gè)程序進(jìn)行編譯
$ emcc example2.cc -o hello.html
瀏覽器打開網(wǎng)頁hello.html,會(huì)顯示val2 == 42。
6、編寫 WebAssembly
上面介紹的是將C、C++編譯成WebAssembly,但是對于習(xí)慣寫Javascript的前端開發(fā)工程師來說成本相對比較大,因此以上的用法適合于將已有的客戶端遷移到Web,對于前端開發(fā)來說,可以采用AssemblyScript。
6.1 為什么選 AssemblyScript 作為 WebAssembly 開發(fā)語言
學(xué)習(xí)成本:
AssemblyScript的語法和TypeScript一致。AssemblyScript 相對于 C、Rust 等其它語言去寫 WebAssembly 而言,學(xué)習(xí)成本低。
兼容性:對于不支持 WebAssembly 的瀏覽器,可以通過 TypeScript 編譯器編譯成可正常執(zhí)行的 JS 代碼,從而實(shí)現(xiàn)從 JS 到 WebAssembly 的平滑遷移。
支持webpack構(gòu)建工具:任何新的 Web 開發(fā)技術(shù)都少不了構(gòu)建流程,webpack支持對
AssemblyScript的構(gòu)建
6.2 AssemblyScript 用法
舉個(gè)??、 用 TypeScript 實(shí)現(xiàn)斐波那契序列計(jì)算的模塊 f.ts 如下:
export function f(x: i32):i32{
if(x===1 || x===2){
return 1
}
return f(x-1)+f(x-2)
}
終端執(zhí)行:asc f.ts -o f.wasm。
就能把以上代碼編譯成可運(yùn)行的 WebAssembly 模塊。
為了加載并執(zhí)行編譯出的 f.wasm 模塊,需要通過 JS 去加載并調(diào)用模塊上的 f 函數(shù),為此需要以下 JS 代碼:
fetch('f.wasm') // 網(wǎng)絡(luò)加載 f.wasm 文件
.then(res => res.arrayBuffer()) // 轉(zhuǎn)成 ArrayBuffer
.then(WebAssembly.instantiate) // 編譯為當(dāng)前 CPU 架構(gòu)的機(jī)器碼 + 實(shí)例化
.then(mod => { // 調(diào)用模塊實(shí)例上的 f 函數(shù)計(jì)算
console.log(mod.instance.f(50));
});
以上代碼中出現(xiàn)了一個(gè)新的內(nèi)置類型 i32,這是 AssemblyScript 在 TypeScript 的基礎(chǔ)上內(nèi)置的類型。 AssemblyScript 和 TypeScript 有細(xì)微區(qū)別,AssemblyScript 是 TypeScript 的子集,為了方便編譯成 WebAssembly 在 TypeScript 的基礎(chǔ)上加了更嚴(yán)格的類型限制, 主要區(qū)別如下:
比 TypeScript 多了很多更細(xì)致的內(nèi)置類型,以優(yōu)化性能和內(nèi)存占用;
不能使用 any 和 undefined 類型,以及枚舉類型;
可空類型的變量必須是引用類型,而不能是基本數(shù)據(jù)類型如 string、number、boolean;
函數(shù)中的可選參數(shù)必須提供默認(rèn)值,函數(shù)必須有返回類型,無返回值的函數(shù)返回類型需要是 void;
不能使用 JS 環(huán)境中的內(nèi)置函數(shù),只能使用 AssemblyScript 提供的內(nèi)置函數(shù)
AssemblyScript 的實(shí)現(xiàn)原理本質(zhì)上是通過 TypeScript 編譯器把 TS 源碼解析成 AST,再把 AST 翻譯成 IR,再通過 LLVM 編譯成 WebAssembly 字節(jié)碼實(shí)現(xiàn)的。
6.3 接入Webpack構(gòu)建
任何新的 Web 開發(fā)技術(shù)都少不了構(gòu)建流程,為了提供一套流暢的 WebAssembly 開發(fā)流程,接下來介紹接入 Webpack 具體步驟。
- 安裝以下依賴,以便讓 TS 源碼被 AssemblyScript 編譯成 WebAssembly。
{
"devDependencies": {
"assemblyscript": "github:AssemblyScript/assemblyscript",
"assemblyscript-typescript-loader": "^1.3.2",
"typescript": "^2.8.1",
"webpack": "^3.10.0",
"webpack-dev-server": "^2.10.1"
}
}
- 修改 webpack.config.js,加入 loader:
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
loader: 'assemblyscript-typescript-loader',
options: {
sourceMap: true,
}
}
]
},
};
3.修改 TypeScript 編譯器配置 tsconfig.json,以便讓 TypeScript 編譯器能支持 AssemblyScript 中引入的內(nèi)置類型和函數(shù)。
{
"extends": "../../node_modules/assemblyscript/std/portable.json",
"include": [
"./**/*.ts"
]
}
- 配置直接繼承自 assemblyscript 內(nèi)置的配置文件。