WebAssembly 完全入門:瞭解 wasm 的前世今身

WebAssembly 完全入門:瞭解 wasm 的前世今身

接觸 WebAssembly 之後,在 google 上看了很多資料。然而,這些資料對 WebAssembly 的使用、介紹、意義等方面的解釋都比較模糊和籠統,總是令人感覺沒有獲得預期的收穫,要麼是因為文章中的例子自己去實操不能成功,要麼就是不知所云、一臉矇蔽。本著業務催生技術的態度,這篇文章就此誕生了。本文主要是對 WebAssembly 的背景做一些介紹,包括 WebAssembly 是怎麼出現的?優勢在哪兒?等等。

WebAssembly 是什麼?

定義

首先我們給它下個定義。

WebAssembly 或者 wasm 是一個可移植、體積小、加載快並且兼容 Web 的全新格式。

例子

當然,我知道,即使你看了定義也不知道 WebAssembly 到底是什麼東西。廢話不多說,我們通過一個簡單的例子來看看 WebAssembly 到底是什麼。

WebAssembly 完全入門:瞭解 wasm 的前世今身

上圖的左側是用 C++ 實現的求遞歸的函數。中間是十六進制的 Binary Code。右側是指令文本。可能有人就問,這跟 WebAssembly 有個屁的關係?其實,中間的十六進制的 Binary Code 就是 WebAssembly。

編譯目標

大家可以看到,其可寫性和可讀性差到無法想象。那是因為 WebAssembly 不是用來給各位用手 一行一行擼 的代碼,WebAssembly 是一個 編譯目標。什麼是編譯目標?當我們寫 TypeScript 的時候,Webpack 最後打包生成的 JavaScript 文件就是編譯目標。可能大家已經猜到了,上圖的 Binary 就是左側的 C++ 代碼經過編譯器編譯之後的結果。

WebAssembly 的由來

性能瓶頸

在業務需求越來越複雜的現在,前端的開發邏輯越來越複雜,相應的代碼量隨之變的越來越多。相應的,整個項目的起步的時間越來越長。在性能不好的電腦上,啟動一個前端的項目甚至要花上十多秒。這些其實還好,說明前端越來越受到重視,越來越多的人開始進行前端的開發。

但是除了邏輯複雜、代碼量大,還有另一個原因是 JavaScript 這門語言本身的缺陷,JavaScript 沒有靜態變量類型。這門解釋型編程語言的作者 Brendan Eich,倉促的創造了這門如果被廣泛使用的語言,以至於 JavaScript 的發展史甚至在某種層面上變成了填坑史。為什麼說沒有靜態類型會降低效率。這會涉及到一些 JavaScript 引擎的一些知識。

靜態變量類型所帶來的問題

WebAssembly 完全入門:瞭解 wasm 的前世今身

這是 Microsoft Edge 瀏覽器的 JavaScript 引擎 ChakraCore 的結構。我們來看一看我們的 JavaScript 代碼在引擎中會經歷什麼。- JavaScript 文件會被下載下來。

  • 然後進入 Parser,Parser 會把代碼轉化成 AST(抽象語法樹)。
  • 然後根據抽象語法樹,Bytecode Compiler 字節碼編譯器會生成引擎能夠直接閱讀、執行的字節碼。
  • 字節碼進入翻譯器,將字節碼一行一行的翻譯成效率十分高的 Machine Code。

在項目運行的過程中,引擎會對執行次數較多的 function 記性優化,引擎將其代碼編譯成 Machine Code 後打包送到頂部的 Just-In-Time(JIT) Compiler,下次再執行這個 function,就會直接執行編譯好的 Machine Code。但是由於 JavaScript 的動態變量,上一秒可能是 Array,下一秒就變成了 Object。那麼上一次引擎所做的優化,就失去了作用,此時又要再一次進行優化。

asm.js 出現

所以為了解決這個問題,WebAssembly 的前身,asm.js 誕生了。asm.js 是一個 Javascript 的嚴格子集,合理合法的 asm.js 代碼一定是合理合法的 JavaScript 代碼,但是反之就不成立。同 WebAssembly 一樣,asm.js 不是用來給各位用手 一行一行擼 的代碼,asm.js 是一個

編譯目標。它的可讀性、可讀性雖然比 WebAssembly 好,但是對於開發者來說,仍然是無法接受的。

asm.js 強制靜態類型,舉個例子。

function asmJs() {
'use asm';
let myInt = 0 | 0;
let myDouble = +1.1;
}

為什麼 asm.js 會有靜態類型呢?因為像0 | 0這樣的,代表這是一個 Int 的數據,而+1.1則代表這是一個 Double 的數據。

asm.js 不能解決所有的問題

可能有人有疑問,這問題不是解決了嗎?那為什麼會有 WebAssembly?WebAssembly 又解決了什麼問題?大家可以再看一下上面的 ChakraCore 的引擎結構。無論 asm.js 對靜態類型的問題做的再好,它始終逃不過要經過 Parser,要經過 ByteCode Compiler,而這兩步是 JavaScript 代碼在引擎執行過程當中消耗時間最多的兩步。而 WebAssembly 不用經過這兩步。這就是 WebAssembly 比 asm.js 更快的原因。

WebAssembly 橫空出世

所以在 2015 年,我們迎來了 WebAssembly。WebAssembly 是經過編譯器編譯之後的代碼,體積小、起步快。在語法上完全脫離 JavaScript,同時具有沙盒化的執行環境。WebAssembly 同樣的強制靜態類型,是 C/C++/Rust 的編譯目標。

WebAssembly 的優勢

WebAssembly 和 asm.js 性能對比

下面的圖是 Unity WebGL 使用和不使用 WebAssembly 的起步時間對比的一個 BenchMark,給大家當作一個參考。可以看到,在 FireFox 中,WebAssembly 和 asm.js 的性能差異達到了 2 倍,在 Chrome 中達到了 3 倍,在 Edge 中甚至達到了 6 倍。通過這些對比也可以從側面看出,目前所有的主流瀏覽器都已經支持 WebAssembly V1(Node >= 8.0.0)。

WebAssembly 完全入門:瞭解 wasm 的前世今身

與 JavaScript 做對比

我自己在一個用create-react-app新建的項目中,分別對比了 WebAssembly 版本和原生 JavaScript 版本的遞歸無優化的 Fibonacci 函數,下圖是這兩個函數在值是 45、48、50 的時候的性能對比。

WebAssembly 完全入門:瞭解 wasm 的前世今身

看圖說話,這就是 WebAssembly 與 JavaScript 很實際的一個性能對比。幾乎穩定的是 JavaScript 的兩倍。

WebAssembly 在大型項目中的應用

在這裡能夠舉的例子還是很多,比如 AutoCAD、GoogleEarth、Unity、Unreal、PSPDKit、WebPack 等等。拿其中幾個來簡單說一下。

AutoCAD

這是一個用於畫圖的軟件,在很長的一段時間是沒有 Web 的版本的,原因有兩個,其一,是 Web 的性能的確不能滿足他們的需求。其二,在 WebAssembly 沒有面世之前,AutoCAD 是用 C++ 實現的,要將其搬到 Web 上,就意味著要重寫他們所有的代碼,這代價十分的巨大。

而在 WebAssembly 面世之後,AutoCAD 得以利用編譯器,將其沉澱了 30 多年的代碼直接編譯成 WebAssembly,同時性能基於之前的普通 Web 應用得到了很大的提升。正是這些原因,得以讓 AutoCAD 將其應用從 Desktop 搬到 Web 中。

Google Earth

Google Earth 也就是谷歌地球,因為需要展示很多 3D 的圖像,對性能要求十分高,所以採取了一些 Native 的技術。最初的時候就連 Google Chrome 瀏覽器都不支持 Web 的版本,需要單獨下載 Google Earth 的 Destop 應用。而在 WebAssembly 之後呢,谷歌地球推出了 Web 的版本。而據說下一個可以運行谷歌地球的瀏覽器是 FireFox。

Unity 和 Unreal 遊戲引擎

這裡給兩個油管的鏈接自己體驗一下。

  • Unity WebGL 的戳這裡: https://youtu.be/rIyIlATjNcE
  • Unreal 引擎的戳這裡: https://www.youtube.com/watch?v=TwuIRcpeUWE

WebAssembly 要取代 JavaScript?

答案是否定的,請看下圖。

WebAssembly 完全入門:瞭解 wasm 的前世今身

大家可以看到這是一個協作關係。WebAssembly 是被設計成 JavaScript 的一個完善、補充,而不是一個替代品。WebAssembly 將很多編程語言帶到了 Web 中。但是 JavaScript 因其不可思議的能力,仍然將保留現有的地位。

什麼時候使用 WebAssembly?

說了這麼多,我到底什麼時候該使用它呢?總結下來,大部分情況分兩個點。

  • 對性能有很高要求的 App/Module/ 遊戲。
  • 在 Web 中使用 C/C++/Rust/Go 的庫舉個簡單的例子。如果你要實現的 Web 版本的 Ins 或者 Facebook, 你想要提高效率。那麼就可以把其中對圖片進行壓縮、解壓縮、處理的工具,用 C++ 實現,然後再編譯回 WebAssembly。

WebAssembly 的幾個開發工具

  • AssemblyScript。支持直接將 TypeScript 編譯成 WebAssembly。這對於很多前端同學來說,入門的門檻還是很低的。地址:https://github.com/AssemblyScript/assemblyscript
  • Emscripten。可以說是 WebAssembly 的靈魂工具不為過,上面說了很多編譯,這個就是那個編譯器。將其他的高級語言,編譯成 WebAssembly。地址:https://github.com/kripken/emscripten
  • WABT。是個將 WebAssembly 在字節碼和文本格式相互轉換的一個工具,方便開發者去理解這個 wasm 到底是在做什麼事。地址:https://github.com/WebAssembly/wabt

WebAssembly 的意義

在我的個人理解上,WebAssembly 並沒有要替代 JavaScript,一統天下的意思。我總結下來就兩個點。

  • 給了 Web 更好的性能。
  • 給了 Web 更多的可能關於 WebAssembly 的性能問題,之前也花了很大的篇幅講過了。而更多的可能,隨著 WebAssembly 的技術越來越成熟,勢必會有更多的應用,從 Desktop 被搬到 Web 上,這會使本來已經十分強大的 Web 更加豐富和強大。

WebAssembly 實操

要進行這個實際操作,你需要安裝上文提到過的編譯器 Emscripten,然後按照 這個步驟去安裝。以下的步驟都默認為你已經安裝了 Emscripten。

安裝步驟: http://webassembly.org.cn/getting-started/developers-guide/

WebAssembly 在 Node 中的應用

導入 Emscripten 環境變量

進入到你的 emscripten 安裝目錄,執行以下代碼。

source emsdk/emsdk_env.sh

新建 C 文件

用 C 實現一個求和文件test.c,如下。

int add(int a, int b) {
return a + b;

}

使用 Emscripten 編譯 C 文件

在同樣的目錄下執行如下代碼。

emcc test.c -Os -s WASM=1 -s SIDE_MODULE=1 -o test.wasm

emcc就是 Emscripten 編譯器,test.c是我們的輸入文件,-Os表示這次編譯需要優化,-s WASM=1表示輸出 wasm 的文件,因為默認的是輸出 asm.js,-s SIDE_MODULE=1表示就只要這一個模塊,不要給我其他亂七八糟的代碼,-o test.wasm是我們的輸出文件。

編譯成功之後,當前目錄下就會生成test.wasm。

編寫在 Node 中調用的代碼

新建一個 js 文件test.js。代碼如下。

const fs = require('fs');
let src = new Uint8Array(fs.readFileSync('./test.wasm'));
const env = {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({
initial: 256
}),
table: new WebAssembly.Table({
initial: 2,
element: 'anyfunc'
}),
abort: () => {throw 'abort';}
}
WebAssembly.instantiate(src, {env: env})
.then(result => {
console.log(result.instance.exports._add(20, 89));
})

.catch(e => console.log(e));

執行 test.js

運行以下代碼。

node test.js

然後就可以看到輸出的結果 109 了。

WebAssembly 在 React 當中的應用

通過 fetch 的方法調用 直接用 fetch 的方式。大概的調用方式如下。

const fibonacciUrl = './fibonacci.wasm';
const {_fibonacci} = await this.getExportFunction(fibonacciUrl);

而getExportFunction具體代碼如下。

getExportFunction = async (url) => {
const env = {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({
initial: 256
}),
table: new WebAssembly.Table({
initial: 2,
element: 'anyfunc'
})
};
const instance = await fetch(url).then((response) => {
return response.arrayBuffer();
}).then((bytes) => {
return WebAssembly.instantiate(bytes, {env: env})
}).then((instance) => {

return instance.instance.exports;
});
return instance;
};

通過 import C 文件來調用

先通過 Import 的方式來引進依賴。

import wasmC from './add.c';

然後進行調用。具體的方式如下。

wasmC({
'global': {},
'env': {
'memoryBase': 0,
'tableBase': 0,
'memory': new WebAssembly.Memory({initial: 256}),
'table': new WebAssembly.Table({initial: 0, element: 'anyfunc'})
}
}).then(result => {
const exports = result.instance.exports;
const add = exports._add;
const fibonacci = exports._fibonacci;
console.log('C return value was', add(2, 5643));
console.log('Fibonacci', fibonacci(2));
});

寫在後面

如今技術出現的越來越多,但是實際上在工作中能夠用到的,越並不是那麼多。其實很多大廠所輸出的一些技術,都是有業務場景的,有業務做推動。而不是憑空造輪子。所以總結下來適合自己的才是最好的。當然不是說不要了解新技術,瞭解新技術跟上步伐是十分必要的。我們現在不用,不代表不需要了解。相反,以後再遇到類似的業務場景時,我們就會多一種選擇,可以更加從容的對待。


分享到:


相關文章: