Web程式效能優化——asm.js和WebAssembly

廣州蘆葦科技web前端發表於2019-01-14

asm.js

asm.jsJavaScript語言中一個可以高度優化的子集。通過避免JavaScript引擎某些難以優化的機制和模式(主要是垃圾回收和型別判斷),達到JavaScript引擎執行優化的目的。換言之,正常的JavaScript程式碼會型別自動裝換和垃圾自動回收的,而編寫asm.js風格的程式碼則表示程式設計師需要管理記憶體和確定資料型別。

asm.js不提供任何額外的語法,只要編寫asm.js的程式碼,在支援asm.js優化的JavaScript引擎中能被自動識別,從而讓引擎實現自己的優化,而在不支援asm.js的引擎中也能正常執行。

手寫asm.js例子

首先解決的是JavaScript中變數型別的問題

var a = 10;
var b = a;
複製程式碼
var a = 10;
var b = a | 0;
複製程式碼

上面兩段程式碼的不同之處在於,第一段程式碼b的值型別在執行的時候才能被確定,而第二段程式碼中b總是會被當作32位整型處理。同樣的還有對運算的限制,如(a + b) | 0把兩個變數的加運算限制為更高效的整型加運算。在支援asm.jsJavaScript引擎中,執行上面的程式碼會進行底層優化。

第二個要解決的問題是記憶體分配,眾所周知,JavaScript不提供垃圾回收相關的API,在JavaScript中講垃圾回收大多數開發者會想到將不用的變數設為null,比如:

// 建立一個長度為10000的陣列
var a = new Array(10000).fill('hello');

a = null;
複製程式碼

但對於垃圾回收a = null只是告訴瀏覽器長度為10000的陣列已經沒有被任何變數引用,可以回收其佔用的記憶體了,至於什麼時候回收,瀏覽器有自己的想法。

asm.js中所說的記憶體分配和這個機制不同,asm.js的記憶體分配由程式設計師自己控制。使用ArrayBuffer建立一個資料緩衝區,可以在這個緩衝區儲存和取值,而不需要再付出記憶體分配和垃圾回收的代價。ArrayBuffer是不能直接操作的,而是通過型別陣列物件和DataView(檢視)物件,具體用法請參考 MDN | ArrayBuffer,下面是一個例子

// 建立一個64k的資料緩衝區
var heap = new ArrayBuffer(0x10000);

// 使用64位浮點值陣列引用這個緩衝區
var arr = new Float64Array( heap )

複製程式碼

下面是一個更復雜的例子,使用asm.js風格的程式碼編寫一個函式計算兩數之間的相鄰數的乘積,儲存起來並計算總和

function ASM (heap) {

  var arr = new Int32Array(heap);

  function foo(x, y) {
    x = x | 0;
    y = y | 0;

    var i = 0;
    var p = 0;
    var sum = 0;
    for (i = x | 0; (i | 0) < (y | 0); p = (p + 8) | 0, i = (i + 1) | 0) {
      sum = (sum + i * (i + 1)) | 0;

      arr[p >> 3] = (i * (i + 1)) | 0;
    }

    return +sum;
  }

  return foo;
}

var heap = new ArrayBuffer(0x1000);

var foo = ASM(heap)

foo(0, 1024) // 357913600

複製程式碼

通常用“模組”機制將asm.js程式碼封裝起來,如上面的程式碼,記憶體區變數為私有變數,不可在外部更改。 上面程式碼中,使用ArrayBuffer分配記憶體,使用Int32Array規定資料型別,在使用資料時,每個資料也都使用特殊的符號標識資料型別,在支援asm.js的引擎中,這些都是觸發優化的訊號,而在不支援asm.js的引擎,這些符號也是正常的運算子而已,不影響計算結果。

Emscripten

在實際運用中,不大可能手寫asm.js規範的程式碼,寫起來異常麻煩並且容易出錯,所以通常asm.js程式碼通常是其他語言的編譯目的碼,比如使用EmscriptenC / C++程式碼編譯成asm.js

安裝Emscripten

$ git clone https://github.com/juj/emsdk.git
$ cd emsdk
$ ./emsdk install latest
$ ./emsdk activate latest
$ source ./emsdk_env.sh

複製程式碼

Emscripten的編譯用法非常簡單

  1. 編寫一個C++程式hello.cc
#include <iostream>

int main () {
  std::cout << "Hello World" << std::endl;
}
複製程式碼
  1. 使用以下命令將C++原始碼編譯成asm.js
emcc hello.cc
複製程式碼

輸出檔案a.out.js就是asm.js規範的JavaScript程式碼,預設執行main函式。

WebAssembly

WebAssembly位元組碼是一種抹平了不同CPU架構的機器碼,WebAssembly位元組碼不能直接在任何一種CPU架構上執行,但由於非常接近機器碼,可以非常快的被翻譯為對應架構的機器碼,因此WebAssembly執行速度和機器碼接近,這聽上去非常像Java位元組碼。

WebAssembly出現之前,瀏覽器只能執行.js字尾的程式設計程式碼檔案,JavaScript是web應用開發的唯一語言,但是在支援WebAssembly的瀏覽器上,現在能執行.wasm字尾的程式碼檔案了。

WebAssembly幾乎不可能是手工編寫的,一般由其他語言編譯而成,目前能編譯成WebAssembly的高階語言有:

  • AssemblyScript: 語法和TypeScript一致,對前端來說學習成本低,為前端編寫WebAssembly最佳選擇;
  • c\c++: 官方推薦的方式;
  • Rust: 語法複雜、學習成本高,對前端來說可能會不適應;
  • Kotlin: 語法和 JavaJS 相似,語言學習成本低;
  • Golang: 語法簡單學習成本低。

Hello world

使用AssemblyScript編譯成WebAssembly,首先安裝

yarn global add AssemblyScript/assemblyscript
複製程式碼

編寫原始碼demo.ts

export function foo (x: i32):i32 {
  return x * x;
}
複製程式碼

使用asc demo.ts -o demo.wasm編譯程式碼,使用js程式碼fetch方法載入wasm模組

fetch('./demo.wasm')
  .then(res => {return res.arrayBuffer()})
  .then(WebAssembly.instantiate) // 編譯成當前CPU架構的機器碼並例項化
  .then(module => { // module為WebAssembly模組
    console.log(module.instance.exports.foo(100))
  })
複製程式碼

總結

asm.jsWebAssembly都是底層優化web程式效能的技術,他們通常都是由其他語言編譯而成。asm.js是JavaScript的一個子集,所以在不支援asm.js優化的瀏覽器上也能正常執行,它的檔案型別是文字;WebAssembly則是更新的技術,提供了新的API,在不支援的瀏覽器上無法執行,它的檔案型別是二進位制位元組碼。這兩種技術雖然都是極高提升web程式效能的技術,但一般開發中不會使用到,只有在密集型計算、圖形處理等計算場景才能發揮出它們的巨大優勢。

作者簡介:葉茂,蘆葦科技web前端開發工程師,代表作品:口紅挑戰網紅小遊戲、蘆葦科技官網。擅長網站建設、公眾號開發、微信小程式開發、小遊戲、公眾號開發,專注於前端框架、服務端渲染、SEO技術、互動設計、影象繪製、資料分析等研究。

歡迎和我們一起並肩作戰: web@talkmoney.cn 訪問 www.talkmoney.cn 瞭解更多

相關文章