Web程式效能優化——asm.js和WebAssembly
asm.js
asm.js
是JavaScript
語言中一個可以高度優化的子集。通過避免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.js
的JavaScript
引擎中,執行上面的程式碼會進行底層優化。
第二個要解決的問題是記憶體分配,眾所周知,JavaScript
不提供垃圾回收相關的API,在JavaScript
中講垃圾回收大多數開發者會想到將不用的變數設為null
// 建立一個長度為10000的陣列
var a = new Array(10000).fill('hello');
a = null;
複製程式碼
但對於垃圾回收a = null
只是告訴瀏覽器長度為10000的陣列已經沒有被任何變數引用,可以回收其佔用的記憶體了,至於什麼時候回收,瀏覽器有自己的想法。
asm.js
中所說的記憶體分配和這個機制不同,asm.js
的記憶體分配由程式設計師自己控制。使用ArrayBuffer
建立一個數據緩衝區,可以在這個緩衝區儲存和取值,而不需要再付出記憶體分配和垃圾回收的代價。ArrayBuffer
是不能直接操作的,而是通過型別陣列物件和DataView
(檢視)物件,具體用法請參考
// 建立一個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
程式碼通常是其他語言的編譯目的碼,比如使用Emscripten
將C / 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的編譯用法非常簡單
#include <iostream>
int main () {
std::cout << "Hello World" << std::endl;
}
複製程式碼
- 使用以下命令將
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: 語法和
Java
、JS
相似,語言學習成本低; - 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.js
和WebAssembly
都是底層優化web程式效能的技術,他們通常都是由其他語言編譯而成。asm.js
是JavaScript的一個子集,所以在不支援asm.js
優化的瀏覽器上也能正常執行,它的檔案型別是文字;WebAssembly
則是更新的技術,提供了新的API,在不支援的瀏覽器上無法執行,它的檔案型別是二進位制位元組碼。這兩種技術雖然都是極高提升web程式效能的技術,但一般開發中不會使用到,只有在密集型計算、圖形處理等計算場景才能發揮出它們的巨大優勢。
作者簡介:葉茂,蘆葦科技web前端開發工程師,代表作品:口紅挑戰網紅小遊戲、蘆葦科技官網。擅長網站建設、公眾號開發、微信小程式開發、小遊戲、公眾號開發,專注於前端框架、服務端渲染、SEO技術、互動設計、影象繪製、資料分析等研究。
歡迎和我們一起並肩作戰: [email protected] 訪問 www.talkmoney.cn 瞭解更多