淺談WebAssembly

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文檔。

簡而言之,工作流程如下所示:

image

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)存。比如,通過XMLHttpRequestFetch,模塊將會(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 具體步驟。

  1. 安裝以下依賴,以便讓 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"
        }
      }
  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"
        ]
      }
  1. 配置直接繼承自 assemblyscript 內(nèi)置的配置文件。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容