1. 程式人生 > >WebAssembly完全入門——瞭解wasm的前世今身

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

前言

接觸WebAssembly之後,在google上看了很多資料。感覺對WebAssembly的使用、介紹、意義都說的比較模糊和籠統。感覺看了之後收穫沒有達到預期,要麼是文章中的例子自己去實操不能成功,要麼就是不知所云、一臉矇蔽。本著業務催生技術的態度,這邊文章就誕生了。前部分主要是對WebAssembly的背景做一些介紹,WebAssembly是怎麼出現的,優勢在哪兒。如果想直接開始擼程式碼試試效果,可以直接跳到最後一個板塊

WebAssembly是什麼?

定義

首先我們給它下個定義。

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

例子

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

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

編譯目標

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

WebAssembly的由來

效能瓶頸

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

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

靜態變數型別所帶來的問題

這是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).

與JavaScript做對比

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

看圖說話,這就是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遊戲引擎

這裡給兩個油管的連結自己體驗一下,大家注意科學上網。

WebAssembly要取代JavaScript?

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

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

什麼時候使用WebAssembly?

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

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

WebAssembly的幾個開發工具

  • AssemblyScript。支援直接將TypeScript編譯成WebAssembly。這對於很多前端同學來說,入門的門檻還是很低的。
  • Emscripten。可以說是WebAssembly的靈魂工具不為過,上面說了很多編譯,這個就是那個編譯器。將其他的高階語言,編譯成WebAssembly。
  • WABT。是個將WebAssembly在位元組碼和文字格式相互轉換的一個工具,方便開發者去理解這個wasm到底是在做什麼事。

WebAssembly的意義

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

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

WebAssembly實操

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

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));
});
複製程式碼

詳細的程式碼在這裡,歡迎Star。

寫在後面

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

關於我