WebAssembly試玩

一.What?


WebAssembly or wasm is a new portable, size- and load-time-efficient format suitable for compilation to the web.

一種可移植,體積小且加載迅速的(二進制)格式,適用於編譯到Web

主要目標是在Web環境支持高性能應用。但設計上不依賴Web特性,也不針對Web特性提供功能,也可以用在其它環境

簡單理解,就是定義了一種編譯目標格式,能在支持該格式的任何環境獲得接近原生的執行性能。相當於允許擴展native模塊,在苛求性能的場景,用其它更合適的語言(比如C++)來實現,再提前編譯到WebAssembly形式,就能獲得媲美native的性能體驗

其設計目標分2方面:

快速,安全和可移植的語義

快速:以接近原生代碼的性能執行,並利用所有現代硬件通用的功能

安全:代碼經過驗證並在內存安全的沙盒環境中執行,防止數據損壞或安全違規

定義良好:充分且精確地定義合法程序及其行爲,以一種容易推斷非正式與正式的方式

獨立於硬件:可在所有現代架構,臺式機或移動設備以及嵌入式系統上進行編譯

獨立於語言:不偏向任何特定語言,編程模型或對象模型

獨立於平臺:可以嵌入到瀏覽器中,作爲stand-alone VM運行,或者集成到其他環境中

開放:程序能夠以簡單通用的方式與他們的環境交互

高效、可移植的表示

小巧:具有比典型文本或原生代碼格式體積更小的二進制格式,能夠快速傳輸

模塊化:程序可以拆分成較小的部分,可以單獨傳輸,緩存和使用

高效:可以在單趟(遍歷)中快速對其進行解碼,驗證和編譯,等同於實時(JIT)或提前(AOT)編譯

流式:允許在拿到所有數據之前,儘早開始解碼、驗證和編譯

可並行:允許將解碼、驗證和編譯拆分成多個獨立的並行任務

可移植:對現代硬件上不受廣泛支持的架構不做假設

由主流瀏覽器(Chrome, Edge, Firefox, and WebKit)合力推動其標準化進程:


WebAssembly is currently being designed as an open standard by a W3C Community Group that includes representatives from all major browsers.

P.S.這個事情由瀏覽器廠商牽頭做(他們4個站在一起搞事情,很值得期待),只是順便建立開放標準(不止面向Web環境),動力源自想要進一步提升JS運行時性能,在V8引入JIT之後,想要進一步提升性能已經不太可能了,因爲面臨JS語言特性方面的限制(比如解釋型,弱類型)。Web能力越來越強大,客戶端JS越來越重,進一步提升JS執行性能的需求仍在,所以纔有了WebAssembly的釜底抽薪

二.wasm與wast
我們知道WebAssembly定義了一種二進制格式,這種格式就是wasm,例如:


0061 736d 0100 0000 0187 8080 8000 0160
027f 7f01 7f03 8280 8080 0001 0004 8480
8080 0001 7000 0005 8380 8080 0001 0001
0681 8080 8000 0007 9080 8080 0002 066d
656d 6f72 7902 0003 6763 6400 000a ab80
8080 0001 a580 8080 0001 017f 0240 2000
450d 0003 4020 0120 0022 026f 2100 2002
2101 2000 0d00 0b20 020f 0b20 010b 

這串十六進制數對應的C代碼是:


// 輾轉相除法求最大公約數
int gcd(int m, int n) {
    if (m == 0) return n;
    return gcd(n % m, m);
}

wasm的可讀性等於0,爲了緩解這個問題,就定義了一種可讀性好一些的文本格式,叫wast:


(module
 (table 0 anyfunc)
 (memory $0 1)
 (export "memory" (memory $0))
 (export "gcd" (func $gcd))
 (func $gcd (; 0 ;) (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (block $label$0
   (br_if $label$0
    (i32.eqz
     (get_local $0)
    )
   )
   (loop $label$1
    (set_local $0
     (i32.rem_s
      (get_local $1)
      (tee_local $2
       (get_local $0)
      )
     )
    )
    (set_local $1
     (get_local $2)
    )
    (br_if $label$1
     (get_local $0)
    )
   )
   (return
    (get_local $2)
   )
  )
  (get_local $1)
 )
)

括號有點Lisp風格,但至少是可讀的,例如:

// 導出了兩個東西,分別叫`memory`和`gcd`
(export "memory" (memory $0))
(export "gcd" (func $gcd))
// 函數簽名,接受2個int32類型參數,返回int32類型值
(func $gcd (; 0 ;) (param $0 i32) (param $1 i32) (result i32)
// 函數體...就不猜了

P.S.wast與wasm能夠互相轉換,詳細見WABT: The WebAssembly Binary Toolkit

另外,在瀏覽器的Source面板能夠看到另一種文本指令:


func (param i32 i32) (result i32)
(local i32)
  block
    get_local 0
    i32.eqz
    br_if 0
    loop
      get_local 1
      get_local 0
      tee_local 2
      i32.rem_s
      set_local 0
      get_local 2
      set_local 1
      get_local 0
      br_if 0
    end
    get_local 2
    return
  end
  get_local 1
end

與wast長得很像,不知道有沒有名字,或者也屬於wast?這個是瀏覽器根據wasm轉換出來的

三.試玩環境
環境要求:

C/C++編譯環境Emscripten

支持WebAssembly的瀏覽器(最新的Chrome默認支持)

在線環境
有無傷試玩環境:WebAssembly Explorer

COMPILE再DOWNLOAD就能得到wasm,簡直好用

注意,默認是C++環境,想用C的話,左側選擇C99或C89,否則函數名會被編壞,例如C++11的wast:


(module
 (table 0 anyfunc)
 (memory $0 1)
 (export "memory" (memory $0))
 (export "_Z3gcdii" (func $_Z3gcdii))
 (func $_Z3gcdii (; 0 ;) (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (block $label$0
   (br_if $label$0
    (i32.eqz
     (get_local $0)
    )
   )
   (loop $label$1
    (set_local $0
     (i32.rem_s
      (get_local $1)
      (tee_local $2
       (get_local $0)
      )
     )
    )
    (set_local $1
     (get_local $2)
    )
    (br_if $label$1
     (get_local $0)
    )
   )
   (return
    (get_local $2)
   )
  )
  (get_local $1)
 )
)

函數名被編成_Z3gcdii了,猜測是命名空間之類的東西在作怪,C++不太熟,乖乖用C

P.S.除了C/C++,其它語言也可以玩WebAssembly,比如Rust

本地環境
下載平臺SDK

按照安裝步驟來做

不出意外的話,到這裏就裝好了,可以emcc -v試一下:


INFO:root:(Emscripten: Running sanity checks)
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.37.22
clang version 4.0.0  (emscripten 1.37.22 : 1.37.22)
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: D:\emsdk-portable-64bit\clang\e1.37.22_64bit
INFO:root:(Emscripten: Running sanity checks)

在Windows環境可能會遇到一個DLL缺失(MSVCP140.dll)的報錯,可以手動安裝需要的C++環境,具體見MSVCP140.dll not found · Issue #5605 · kripken/emscripten

然後可以編一個試試(把之前的C代碼保存成文件gcd.c):

emcc ./c/gcd.c -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o ./output/gcd.wasm

P.S.更多用法見Emscripten Tutorial

得到的gcd.wasm內容如下:


0061 736d 0100 0000 000c 0664 796c 696e
6b80 80c0 0200 010a 0260 027f 7f01 7f60
0000 0241 0403 656e 760a 6d65 6d6f 7279
4261 7365 037f 0003 656e 7606 6d65 6d6f
7279 0200 8002 0365 6e76 0574 6162 6c65
0170 0000 0365 6e76 0974 6162 6c65 4261
7365 037f 0003 0403 0001 0106 0b02 7f01
4100 0b7f 0141 000b 072b 0312 5f5f 706f
7374 5f69 6e73 7461 6e74 6961 7465 0002
0b72 756e 506f 7374 5365 7473 0001 045f
6763 6400 0009 0100 0a40 0327 0101 7f20
0004 4003 4020 0120 006f 2202 0440 2000
2101 2002 2100 0c01 0b0b 0520 0121 000b
2000 0b03 0001 0b12 0023 0024 0223 0241
8080 c002 6a24 0310 010b 

注意,方法名默認會被添上下劃線(_)前綴,本例中導出的方法名爲_gcd,具體見Interacting with code:


The keys passed into mergeInto generate functions that are prefixed by _. In other words myfunc: function() {}, becomes function _myfunc() {}, as all C methods in emscripten have a _ prefix. Keys starting with $ have the $ stripped and no underscore added.

在JS中使用模塊接口應該加上下劃線(不知道有沒有配置項能去掉它)

四.試玩


WebAssembly.compile(new Uint8Array(`
    0061 736d 0100 0000 0187 8080 8000 0160
    027f 7f01 7f03 8280 8080 0001 0004 8480
    8080 0001 7000 0005 8380 8080 0001 0001
    0681 8080 8000 0007 9080 8080 0002 066d
    656d 6f72 7902 0003 6763 6400 000a ab80
    8080 0001 a580 8080 0001 017f 0240 2000
    450d 0003 4020 0120 0022 026f 2100 2002
    2101 2000 0d00 0b20 020f 0b20 010b 
    `.match(/\S{2}/g).map(s => parseInt(s, 16))
)).then(module => {
    const instance = new WebAssembly.Instance(module);
    console.log(instance.exports);
    const { gcd } = instance.exports;
    console.log('gcd(328, 648)', gcd(328, 648));
});

其中十六進制串來自在線試玩,與最初的wasm示例內容一致。把這些東西粘到Chrome的Console執行就可以了,一切正常的話,會得到報錯:


VM40:1 Uncaught (in promise) CompileError: WasmCompile: Wasm code generation disallowed in this context

這是因爲默認的CSP(內容安全策略)限制,很容易解決,開隱身模式(Ctrl/CMD + Shift + N)即可

會得到輸出:


{memory: Memory, gcd: ƒ}
gcd(328, 648) 8

第一行是加載我們的WebAssembly得到的模塊導出內容,包括一個內存對象和gcd方法,第二行輸出就是調用高性能模塊計算出的最大公約數

WebAssembly.compile等相關API可以參考:

JavaScript API – WebAssembly:規範定義

WebAssembly – JavaScript | MDN:含有示例

另外,本地編譯得到的版本要求imports env(而且函數名被添了下劃線_前綴):


WebAssembly.compile(new Uint8Array(`
    0061 736d 0100 0000 000c 0664 796c 696e
    6b80 80c0 0200 010a 0260 027f 7f01 7f60
    0000 0241 0403 656e 760a 6d65 6d6f 7279
    4261 7365 037f 0003 656e 7606 6d65 6d6f
    7279 0200 8002 0365 6e76 0574 6162 6c65
    0170 0000 0365 6e76 0974 6162 6c65 4261
    7365 037f 0003 0403 0001 0106 0b02 7f01
    4100 0b7f 0141 000b 072b 0312 5f5f 706f
    7374 5f69 6e73 7461 6e74 6961 7465 0002
    0b72 756e 506f 7374 5365 7473 0001 045f
    6763 6400 0009 0100 0a40 0327 0101 7f20
    0004 4003 4020 0120 006f 2202 0440 2000
    2101 2002 2100 0c01 0b0b 0520 0121 000b
    2000 0b03 0001 0b12 0023 0024 0223 0241
    8080 c002 6a24 0310 010b 
    `.match(/\S{2}/g).map(s => parseInt(s, 16))
)).then(module => {
    let imports = {
        env: {
            memoryBase: 0,
            memory: new WebAssembly.Memory({ initial: 256 }),
            tableBase: 0,
            table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
        }
    };
    const instance = new WebAssembly.Instance(module, imports);
    console.log(instance.exports);
    // 注意下劃線前綴
    const { _gcd } = instance.exports;
    console.log('gcd(328, 648)', _gcd(328, 648));
});

可以得到類似輸出:


{__post_instantiate: ƒ, runPostSets: ƒ, _gcd: ƒ}
gcd(328, 648) 8

應該是Emscripten默認添了一些無關緊要的東西,功能上與我們的簡版是等價的

五.優缺點及應用場景
優勢
代碼體積很小

300k左右(壓縮後)JavaScript 邏輯改用WebAssembly重寫後,體積僅有90k左右

但使用WebAssembly需要引入一個50k-100k的JavaScript類庫作爲基礎設施

安全性稍有提升

雖然源碼對應的WebAssembly文本指令仍然毫無遮掩,但逆向成本高了一些

性能提升

理論上WebAssembly擁有接近native的執行性能,因爲跳過了解釋環節,並且文件體積在傳輸方面也有優勢

當然,前提是在業務代碼量很大,且要求極致性能的場景,在benchmark等重複執行的場景,JIT並不比AOT慢多少

缺點
目前能力有限:

僅支持幾種基本數據類型(i32 / i64 / f32 / f64 / i8 / i16)

無法直接訪問DOM和其它Web API

無法控制GC

應用場景
WebAssembly爲瀏覽器定義了一種標準可執行二進制格式,這樣更多的開發者都能通過統一的編譯機制參與進來,共建繁榮的Web生態,願景是美好的,但面臨一些實際問題

首先WebAssembly的初衷是“在Web環境支持高性能應用”,爲了突破性能瓶頸,那麼可能的應用場景是:

視頻解碼

圖像處理

3D/WebVR/AR可視化

渲染引擎

物理引擎

壓縮/加密算法

…等運算量比較大的場景

當然,些支持將來也可能會都內置到瀏覽器裏,而不用通過“擴展插件”之類的方式來做。但WebAssembly的真正意義是提供了一種允許自行擴展高性能“native”模塊的能力,畢竟等瀏覽器提供,再等到兼容性可接受可能需要相當長的一段時間,而有了這種能力之後,不用再苦苦等待市場主流瀏覽器都支持某個原生特性了,自己動手就搞定了,而且不存在兼容性差異。反過來,可能湧現出一批受歡迎的社區模塊,並逐步被吸納作爲瀏覽器原生支持,生態回饋Web環境

參考資料
WebAssembly

WebAssembly 實踐:如何寫代碼:很不錯的入門指南

如何評論瀏覽器最新的 WebAssembly 字節碼技術?

WebAssembly:解決 JavaScript 痼疾的銀彈?

WebAssembly,Web的新時代

Can WebAssembly be polyfilled?

wasm-arrays:WebAssembly數組包裝庫

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