前一篇我們說了要解決高效能運算的兩個方法,一個是併發用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應該是一個前景很好的專案。我們可以看一下目前瀏覽器的支援情況:
安裝Emscripten
訪問https://kripken.github.io/emscripten-site/docs/getting_started/downloads.html
1. 下載對應平臺版本的SDK
2. 通過emsdk獲取最新版工具
1 2 3 4 5 6 7 8 9 10 11 |
# Fetch the latest registry of available tools. ./emsdk update # Download and install the latest SDK tools. ./emsdk install latest # Make the "latest" SDK "active" for the current user. (writes ~/.emscripten file) ./emsdk activate latest # Activate PATH and other environment variables in the current terminal source ./emsdk_env.sh |
3. 將下列新增到環境變數PATH中
1 2 3 |
~/emsdk-portable ~/emsdk-portable/clang/fastcomp/build_incoming_64/bin ~/emsdk-portable/emscripten/incoming |
4. 其他
我在執行的時候碰到報錯說LLVM
版本不對,後來參考文件配置了LLVM_ROOT
變數就好了,如果你沒有遇到問題,可以忽略。
1 |
LLVM_ROOT = os.path.expanduser(os.getenv('LLVM', '/home/ubuntu/a-path/emscripten-fastcomp/build/bin')) |
5. 驗證是否安裝好
執行emcc -v
,如果安裝好會出現如下資訊:
1 2 3 4 5 6 |
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.37.21 clang version 4.0.0 (https://github.com/kripken/emscripten-fastcomp-clang.git 974b55fd84ca447c4297fc3b00cefb6394571d18) (https://github.com/kripken/emscripten-fastcomp.git 9e4ee9a67c3b67239bd1438e31263e2e86653db5) (emscripten 1.37.21 : 1.37.21) Target: x86_64-apple-darwin15.5.0 Thread model: posix InstalledDir: /Users/magicly/emsdk-portable/clang/fastcomp/build_incoming_64/bin INFO:root:(Emscripten: Running sanity checks) |
Hello, WebAssembly!
建立一個檔案hello.c
:
1 2 3 4 5 |
#include <stdio.h> int main() { printf("Hello, WebAssembly!\n"); return 0; } |
編譯C/C++
程式碼:
1 |
emcc hello.c |
上述命令會生成一個a.out.js
檔案,我們可以直接用Node.js
執行:
1 |
node a.out.js |
輸出
1 |
Hello, WebAssembly! |
為了讓程式碼執行在網頁裡面,執行下面命令會生成hello.html
和hello.js
兩個檔案,其中hello.js
和a.out.js
內容是完全一樣的。
1 2 |
emcc hello.c -o hello.html<code> |
1 2 3 4 |
➜ webasm-study md5 a.out.js MD5 (a.out.js) = d7397f44f817526a4d0f94bc85e46429 ➜ webasm-study md5 hello.js MD5 (hello.js) = d7397f44f817526a4d0f94bc85e46429 |
前面生成的程式碼都是asm.js
,畢竟Emscripten是人家作者Alon Zakai最早用來生成asm.js
的,預設輸出asm.js
也就不足為奇了。當然,可以通過option生成wasm
,會生成三個檔案:hello-wasm.html
, hello-wasm.js
, hello-wasm.wasm
。
1 |
emcc hello.c -s WASM=1 -o hello-wasm.html |
然後瀏覽器開啟hello-wasm.html
,發現報錯TypeError: Failed to fetch
。原因是wasm
檔案是通過XHR
非同步載入的,用file:////
訪問會報錯,所以我們需要啟一個伺服器。
1 2 |
npm install -g serve serve |
然後訪問http://localhost:5000/hello-wasm.html
,就可以看到正常結果了。
呼叫C/C++函式
前面的Hello, WebAssembly!
都是main
函式直接打出來的,而我們使用WebAssembly
的目的是為了高效能運算,做法多半是用C/C++實現某個函式進行耗時的計算,然後編譯成wasm
,暴露給js去呼叫。
在檔案add.c
中寫如下程式碼:
1 2 3 4 5 6 7 8 9 |
#include <stdio.h> int add(int a, int b) { return a + b; } int main() { printf("a + b: %d", add(1, 2)); return 0; } |
有兩種方法可以把add
方法暴露出來給js呼叫。
通過命令列引數暴露API
1 |
emcc -s EXPORTED_FUNCTIONS="['_add']" add.c -o add.js |
注意方法名add
前必須加_
。 然後我們可以在Node.js
裡面這樣使用:
1 2 3 |
// file node-add.js const add_module = require('./add.js'); console.log(add_module.ccall('add', 'number', ['number', 'number'], [2, 3])); |
執行node node-add.js
會輸出5
。 如果需要在web頁面使用的話,執行:
1 |
emcc -s EXPORTED_FUNCTIONS="['_add']" add.c -o add.html |
然後在生成的add.html
中加入如下程式碼:
1 2 3 4 5 6 7 |
<button onclick="nativeAdd()">click</button> <script type='text/javascript'> function nativeAdd() { const result = Module.ccall('add', 'number', ['number', 'number'], [2, 3]); alert(result); } </script> |
然後點選button,就可以看到執行結果了。
Module.ccall
會直接呼叫C/C++
程式碼的方法,更通用的場景是我們獲取到一個包裝過的函式,可以在js裡面反覆呼叫,這需要用Module.cwrap
,具體細節可以參看文件。
1 2 3 |
const cAdd = add_module.cwrap('add', 'number', ['number', 'number']); console.log(cAdd(2, 3)); console.log(cAdd(2, 4)); |
定義函式的時候新增EMSCRIPTEN_KEEPALIVE
新增檔案add2.c
。
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> #include <emscripten.h> int EMSCRIPTEN_KEEPALIVE add(int a, int b) { return a + b; } int main() { printf("a + b: %d", add(1, 2)); return 0; } |
執行命令:
1 |
emcc add2.c -o add2.html |
同樣在add2.html
中新增程式碼:
1 2 3 4 5 6 7 |
<button onclick="nativeAdd()">click</button> <script type='text/javascript'> function nativeAdd() { const result = Module.ccall('add', 'number', ['number', 'number'], [2, 3]); alert(result); } </script> |
但是,當你點選button的時候,報錯:
1 |
Assertion failed: the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits) |
可以通過在main()
中新增emscripten_exit_with_live_runtime()
解決:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> #include <emscripten.h> int EMSCRIPTEN_KEEPALIVE add(int a, int b) { return a + b; } int main() { printf("a + b: %d", add(1, 2)); emscripten_exit_with_live_runtime(); return 0; } |
或者也可以直接在命令列中新增-s NO_EXIT_RUNTIME=1
來解決,
1 |
emcc add2.c -o add2.js -s NO_EXIT_RUNTIME=1 |
不過會報一個警告:
1 |
exit(0) implicitly called by end of main(), but noExitRuntime, so not exiting the runtime (you can use emscripten_force_exit, if you want to force a true shutdown)exit(0) implicitly called by end of main(), but noExitRuntime, so not exiting the runtime (you can use emscripten_force_exit, if you want to force a true shutdown) |
所以建議採用第一種方法。
上述生成的程式碼都是asm.js
,只需要在編譯引數中新增-s WASM=1
中就可以生成wasm
,然後使用方法都一樣。
用asm.js和WebAssembly執行耗時計算
前面準備工作都做完了, 現在我們來試一下用C
程式碼來優化前一篇中提過的問題。程式碼很簡單:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// file sum.c #include <stdio.h> // #include <emscripten.h> long sum(long start, long end) { long total = 0; for (long i = start; i <= end; i += 3) { total += i; } for (long i = start; i <= end; i += 3) { total -= i; } return total; } int main() { printf("sum(0, 1000000000): %ld", sum(0, 1000000000)); // emscripten_exit_with_live_runtime(); return 0; } |
注意用gcc
編譯的時候需要把跟emscriten
相關的兩行程式碼註釋掉,否則編譯不過。 我們先直接用gcc
編譯成native code
看看程式碼執行多塊呢?
1 2 3 4 5 6 7 8 9 |
➜ webasm-study gcc sum.c ➜ webasm-study time ./a.out sum(0, 1000000000): 0./a.out 5.70s user 0.02s system 99% cpu 5.746 total ➜ webasm-study gcc -O1 sum.c ➜ webasm-study time ./a.out sum(0, 1000000000): 0./a.out 0.00s user 0.00s system 64% cpu 0.003 total ➜ webasm-study gcc -O2 sum.c ➜ webasm-study time ./a.out sum(0, 1000000000): 0./a.out 0.00s user 0.00s system 64% cpu 0.003 total |
可以看到有沒有優化差別還是很大的,優化過的程式碼執行時間是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的優化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <stdio.h> // #include <emscripten.h> // long EMSCRIPTEN_KEEPALIVE sum(long start, long end) { long sum(long start, long end) { long total = 0; for (long i = start; i <= end; i += 1) { if (i % 2 == 0 || i % 3 == 1) { total += i; } else if (i % 5 == 0 || i % 7 == 1) { total += i / 2; } } for (long i = start; i <= end; i += 1) { if (i % 2 == 0 || i % 3 == 1) { total -= i; } else if (i % 5 == 0 || i % 7 == 1) { total -= i / 2; } } return total; } int main() { printf("sum(0, 1000000000): %ld", sum(0, 100000000)); // emscripten_exit_with_live_runtime(); return 0; } |
執行結果大概要正常一些了。
1 2 3 |
➜ webasm-study gcc -O2 sum.c ➜ webasm-study time ./a.out sum(0, 1000000000): 0./a.out 0.32s user 0.00s system 99% cpu 0.324 total |
ok,我們來編譯成asm.js
了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <stdio.h> #include <emscripten.h> long EMSCRIPTEN_KEEPALIVE sum(long start, long end) { // long sum(long start, long end) { long total = 0; for (long i = start; i <= end; i += 1) { if (i % 2 == 0 || i % 3 == 1) { total += i; } else if (i % 5 == 0 || i % 7 == 1) { total += i / 2; } } for (long i = start; i <= end; i += 1) { if (i % 2 == 0 || i % 3 == 1) { total -= i; } else if (i % 5 == 0 || i % 7 == 1) { total -= i / 2; } } return total; } int main() { printf("sum(0, 1000000000): %ld", sum(0, 100000000)); emscripten_exit_with_live_runtime(); return 0; } |
執行
1 |
emcc sum.c -o sum.html |
然後在sum.html
中新增程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<button onclick="nativeSum()">NativeSum</button> <button onclick="jsSumCalc()">JSSum</button> <script type='text/javascript'> function nativeSum() { t1 = Date.now(); const result = Module.ccall('sum', 'number', ['number', 'number'], [0, 100000000]); t2 = Date.now(); console.log(`result: ${result}, cost time: ${t2 - t1}`); } </script> <script type='text/javascript'> function jsSum(start, end) { let total = 0; for (let i = start; i <= end; i += 1) { if (i % 2 == 0 || i % 3 == 1) { total += i; } else if (i % 5 == 0 || i % 7 == 1) { total += i / 2; } } for (let i = start; i <= end; i += 1) { if (i % 2 == 0 || i % 3 == 1) { total -= i; } else if (i % 5 == 0 || i % 7 == 1) { total -= i / 2; } } return total; } function jsSumCalc() { const N = 100000000;// 總次數1億 t1 = Date.now(); result = jsSum(0, N); t2 = Date.now(); console.log(`result: ${result}, cost time: ${t2 - t1}`); } </script> |
另外,我們修改成編譯成WebAssembly看看效果呢?
1 |
emcc sum.c -o sum.js -s WASM=1 |
Browser | webassembly | asm.js | js |
---|---|---|---|
Chrome61 | 1300ms | 600ms | 3300ms |
Firefox55 | 600ms | 800ms | 700ms |
Safari9.1 | 不支援 | 2800ms | 因不支援ES6我懶得改寫沒測試 |
感覺Firefox有點不合理啊, 預設的JS太強了吧。然後覺得webassembly也沒有特別強啊,突然發現emcc
編譯的時候沒有指定優化選項-O2
。再來一次:
1 2 |
emcc -O2 sum.c -o sum.js # for asm.js emcc -O2 sum.c -o sum.js -s WASM=1 # for webassembly |
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
- http://asmjs.org/
- http://webassembly.org/
- https://kripken.github.io/emscripten-site/index.html
- https://developer.mozilla.org/en-US/docs/WebAssembly
- http://www.codepool.biz/emscripten-compile-cc-javascript.html
- http://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
- https://zhuanlan.zhihu.com/p/25865972