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

前言

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

WebAssembly是什麼?

定義

首先我們給它下個定義。

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

例子

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

clipboard.png

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

編譯目標

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

WebAssembly的由來

性能瓶頸

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

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

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

clipboard.png

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

與JavaScript做對比

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

clipboard.png

看圖說話,這就是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?

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

clipboard.png

大家可以看到這是一個協作關係。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。

寫在後面

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

關於我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章