前端高效能運算之二:asm.js & webassembly

發表於2017-10-21

前一篇我們說了要解決高效能運算的兩個方法,一個是併發用WebWorkers,另一個就是用更底層的靜態語言。

2012年,Mozilla的工程師Alon Zakai在研究LLVM編譯器時突發奇想:能不能把C/C++編譯成Javascript,並且儘量達到Native程式碼的速度呢?於是他開發了Emscripten編譯器,用於將C/C++程式碼編譯成Javascript的一個子集asm.js,效能差不多是原生程式碼的50%。大家可以看看這個PPT

之後Google開發了Portable Native Client,也是一種能讓瀏覽器執行C/C++程式碼的技術。 後來估計大家都覺得各搞各的不行啊,居然Google, Microsoft, Mozilla, Apple等幾家大公司一起合作開發了一個面向Web的通用二進位制和文字格式的專案,那就是WebAssembly,官網上的介紹是:

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

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

所以,WebAssembly應該是一個前景很好的專案。我們可以看一下目前瀏覽器的支援情況caniuse-webassembly

安裝Emscripten

訪問https://kripken.github.io/emscripten-site/docs/getting_started/downloads.html

1. 下載對應平臺版本的SDK

2. 通過emsdk獲取最新版工具

3. 將下列新增到環境變數PATH中

4. 其他

我在執行的時候碰到報錯說LLVM版本不對,後來參考文件配置了LLVM_ROOT變數就好了,如果你沒有遇到問題,可以忽略。

5. 驗證是否安裝好

執行emcc -v,如果安裝好會出現如下資訊:

Hello, WebAssembly!

建立一個檔案hello.c

編譯C/C++程式碼:

上述命令會生成一個a.out.js檔案,我們可以直接用Node.js執行:

輸出

為了讓程式碼執行在網頁裡面,執行下面命令會生成hello.htmlhello.js兩個檔案,其中hello.jsa.out.js內容是完全一樣的。

然後在瀏覽器開啟hello.html,可以看到頁面 hello1

前面生成的程式碼都是asm.js,畢竟Emscripten是人家作者Alon Zakai最早用來生成asm.js的,預設輸出asm.js也就不足為奇了。當然,可以通過option生成wasm,會生成三個檔案:hello-wasm.html, hello-wasm.js, hello-wasm.wasm

然後瀏覽器開啟hello-wasm.html,發現報錯TypeError: Failed to fetch。原因是wasm檔案是通過XHR非同步載入的,用file:////訪問會報錯,所以我們需要啟一個伺服器。

然後訪問http://localhost:5000/hello-wasm.html,就可以看到正常結果了。

呼叫C/C++函式

前面的Hello, WebAssembly!都是main函式直接打出來的,而我們使用WebAssembly的目的是為了高效能運算,做法多半是用C/C++實現某個函式進行耗時的計算,然後編譯成wasm,暴露給js去呼叫。

在檔案add.c中寫如下程式碼:

有兩種方法可以把add方法暴露出來給js呼叫。

通過命令列引數暴露API

注意方法名add前必須加_。 然後我們可以在Node.js裡面這樣使用:

執行node node-add.js會輸出5。 如果需要在web頁面使用的話,執行:

然後在生成的add.html中加入如下程式碼:

然後點選button,就可以看到執行結果了。

Module.ccall會直接呼叫C/C++程式碼的方法,更通用的場景是我們獲取到一個包裝過的函式,可以在js裡面反覆呼叫,這需要用Module.cwrap,具體細節可以參看文件

定義函式的時候新增EMSCRIPTEN_KEEPALIVE

新增檔案add2.c

執行命令:

同樣在add2.html中新增程式碼:

但是,當你點選button的時候,報錯:

可以通過在main()中新增emscripten_exit_with_live_runtime()解決:

或者也可以直接在命令列中新增-s NO_EXIT_RUNTIME=1來解決,

不過會報一個警告:

所以建議採用第一種方法。

上述生成的程式碼都是asm.js,只需要在編譯引數中新增-s WASM=1中就可以生成wasm,然後使用方法都一樣。

用asm.js和WebAssembly執行耗時計算

前面準備工作都做完了, 現在我們來試一下用C程式碼來優化前一篇中提過的問題。程式碼很簡單:

注意用gcc編譯的時候需要把跟emscriten相關的兩行程式碼註釋掉,否則編譯不過。 我們先直接用gcc編譯成native code看看程式碼執行多塊呢?

可以看到有沒有優化差別還是很大的,優化過的程式碼執行時間是3ms!。really?仔細想想,我for迴圈了10億次啊,每次for執行大概是兩次加法,兩次賦值,一次比較,而我總共做了兩次for迴圈,也就是說至少是100億次操作,而我的mac pro是2.5 GHz Intel Core i7,所以1s應該也就執行25億次CPU指令操作吧,怎麼可能逆天到這種程度,肯定是哪裡錯了。想起之前看到的一篇rust測試效能的文章,說rust直接在編譯的時候算出了答案, 然後把結果直接寫到了編譯出來的程式碼裡, 不知道gcc是不是也做了類似的事情。在知乎上GCC中-O1 -O2 -O3 優化的原理是什麼?這篇文章裡, 還真有loop-invariant code motion(LICM)針對for的優化,所以我把程式碼增加了一些if判斷,希望能“糊弄”得了gcc的優化。

執行結果大概要正常一些了。

ok,我們來編譯成asm.js了。

執行

然後在sum.html中新增程式碼

另外,我們修改成編譯成WebAssembly看看效果呢?

Browser webassembly asm.js js
Chrome61 1300ms 600ms 3300ms
Firefox55 600ms 800ms 700ms
Safari9.1 不支援 2800ms 因不支援ES6我懶得改寫沒測試

感覺Firefox有點不合理啊, 預設的JS太強了吧。然後覺得webassembly也沒有特別強啊,突然發現emcc編譯的時候沒有指定優化選項-O2。再來一次:

Browser webassembly -O2 asm.js -O2 js
Chrome61 1300ms 600ms 3300ms
Firefox55 650ms 630ms 700ms

居然沒什麼變化, 大失所望。號稱asm.js可以達到native的50%速度麼,這個倒是好像達到了。但是今年Compiling for the Web with WebAssembly (Google I/O ‘17)裡說WebAssembly是1.2x slower than native code,感覺不對呢。asm.js還有一個好處是,它就是js,所以即使瀏覽器不支援,也能當成不同的js執行,只是沒有加速效果。當然WebAssembly受到各大廠商一致推崇,作為一個新的標準,肯定前景會更好,期待會有更好的表現。

Rust

本來還想寫Rust編譯成WebAssembly的,不過感覺本文已經太長了, 後期再寫如果結合Rust做WebAssembly吧。

著急的可以先看看這兩篇

Refers

相關文章