以前、WebWorkers-WEB のハイパフォーマンス コンピューティングについて紹介しましたが、JavaScript に関する知識は非常に興味深いので、今日は asm.js と webassembly と WEB のハイパフォーマンス コンピューティングの関係についてお話します。その前に、高パフォーマンスの問題を解決する必要があります。 計算には 2 つの方法があり、1 つは WebWorkers を同時に使用する方法、もう 1 つは低レベルの静的言語を使用する方法です。
2012 年、Mozilla エンジニアの Alon Zakai は、LLVM コンパイラーを研究していたときに突然思いつきました。C/C++ を Javascript にコンパイルして、ネイティブ コードの速度を実現できるだろうか?そこで彼は、C/C++ コードを Javascript のサブセットである asm.js にコンパイルするために使用される Emscripten コンパイラを開発しました。そのパフォーマンスはネイティブ コードのほぼ 50% です。このPPTをご覧ください。
その後、Google はポータブル ネイティブ クライアントを開発しました。これもブラウザで C/C++ コードを実行できるようにするテクノロジーです。 その後、誰もが自分たちのことをやるのは不可能だと感じたと思います。実際、Google、Microsoft、Mozilla、Apple などの大手企業が協力して、Web 用のユニバーサル バイナリおよびテキスト形式のプロジェクトを開発しました。それが WebAssembly です。公式 Web サイトには次のようにあります:
WebAssembly または wasm は、Web へのコンパイルに適した、ポータブルでサイズとロード時間の効率が高い新しい形式です。
WebAssembly は現在、によってオープン標準として設計されています。すべての主要なブラウザの代表者が含まれる W3C コミュニティ グループです。
したがって、WebAssembly は将来性の高いプロジェクトになるはずです。現在のブラウザのサポートを確認できます:
Emscripten をインストールします
https://kripken.github.io/emscripten-site/docs/getting_started/downloads にアクセスしてください.html
1. プラットフォームのバージョンに対応する SDK をダウンロードします
2. emsdk を通じてツールの最新バージョンを入手します
3. 環境変数 PATH
# 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
4. その他
実行中にLLVMのバージョンが間違っているというエラーが発生しました。後はドキュメントを参照してLLVM_ROOT変数を設定するだけです。問題がなければ、無視しても構いません。
~/emsdk-portable ~/emsdk-portable/clang/fastcomp/build_incoming_64/bin ~/emsdk-portable/emscripten/incoming
5. インストールされているかどうかを確認します
インストールされている場合は、次のメッセージが表示されます:
LLVM_ROOT = os.path.expanduser(os.getenv('LLVM', '/home/ubuntu/a-path/emscripten-fastcomp/build/bin'))
Hello, WebAssembly!
ファイルを作成します.c:
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)
C/C++ コードをコンパイルします:
#include <stdio.h> int main() { printf("Hello, WebAssembly!\n"); return 0; }
上記のコマンドは、Node.js で直接実行できる a.out.js ファイルを生成します:
emcc hello.c
Output
node a.out.js
Web ページ上のコードで、次のコマンドを実行すると、hello.html と hello.js の 2 つのファイルが生成されます。hello.js と a.out.js の内容はまったく同じです。
Hello, WebAssembly!
その後、ブラウザで hello.html を開くと、ページが表示されます
以前に生成されたコードはすべて asm.js です 結局のところ、Emscripten は作者 Alon Zakai によって最初に asm.js を生成されました。 、デフォルトの出力が asm.js であるのも不思議ではありません。もちろんオプションでwasmを生成することも可能で、hello-wasm.html、hello-wasm.js、hello-wasm.wasmの3つのファイルが生成されます。
emcc hello.c -o hello.html<code> ➜ webasm-study md5 a.out.js MD5 (a.out.js) = d7397f44f817526a4d0f94bc85e46429 ➜ webasm-study md5 hello.js MD5 (hello.js) = d7397f44f817526a4d0f94bc85e46429
その後、ブラウザで hello-wasm.html が開かれ、TypeError: Failed to fetch というエラーが見つかりました。理由は、wasm ファイルが XHR 経由で非同期に読み込まれるため、
file://// を使用してアクセスするとエラーが報告されるため、サーバーを起動する必要があります。 emcc hello.c -s WASM=1 -o hello-wasm.html
C/C++ 関数の呼び出し
これまでの Hello, WebAssembly! はすべて main 関数によって直接型付けされており、WebAssembly を使用する目的は、ほとんどのメソッドが C/C++ を使用して実装することです。時間のかかる特定の関数 その後、計算は wasm にコンパイルされ、呼び出すために js に公開されます。
ファイル add.c に次のコードを記述します。
npm install -g serve serve
js 呼び出しの add メソッドを公開するには 2 つの方法があります。
コマンドラインパラメータを通じて API を公開する
#include <stdio.h> int add(int a, int b) { return a + b; } int main() { printf("a + b: %d", add(1, 2)); return 0; }
メソッド名 add の前に _ を追加する必要があることに注意してください。 次に、これを Node.js で次のように使用できます:
emcc -s EXPORTED_FUNCTIONS="['_add']" add.c -o add.js
ノードnode-add.jsを実行すると、5が出力されます。 Web ページで使用する必要がある場合は、次のコードを実行します。
// file node-add.js const add_module = require('./add.js'); console.log(add_module.ccall('add', 'number', ['number', 'number'], [2, 3]));
次に、生成された add.html に次のコードを追加します。
emcc -s EXPORTED_FUNCTIONS="['_add']" add.c -o add.html
次に、ボタンをクリックして実行結果を確認します。
Module.ccall は C/C++ コード メソッドを直接呼び出します。これには、Module.cwrap が必要です。詳細については、ドキュメントを参照してください。
<button onclick="nativeAdd()">click</button> <script type='text/javascript'> function nativeAdd() { const result = Module.ccall('add', 'number', ['number', 'number'], [2, 3]); alert(result); } </script>
const cAdd = add_module.cwrap('add', 'number', ['number', 'number']); console.log(cAdd(2, 3)); console.log(cAdd(2, 4));
#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; }
emcc add2.c -o add2.html
Assertion failed: the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)
可以通过在main()中添加emscripten_exit_with_live_runtime()解决:
#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来解决,
emcc add2.c -o add2.js -s NO_EXIT_RUNTIME=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代码来优化前一篇中提过的问题。代码很简单:
// 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看看代码运行多块呢?
➜ 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的优化。
#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; }
执行结果大概要正常一些了。
➜ 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了。
#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; } 执行 emcc sum.c -o sum.html
然后在sum.html中添加代码
<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看看效果呢? emcc sum.c -o sum.js -s WASM=1
感觉Firefox有点不合理啊, 默认的JS太强了吧。然后觉得webassembly也没有特别强啊,突然发现emcc编译的时候没有指定优化选项-O2。再来一次:
emcc -O2 sum.c -o sum.js # for asm.js emcc -O2 sum.c -o sum.js -s WASM=1 # for webassembly
居然没什么变化, 大失所望。号称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受到各大厂商一致推崇,作为一个新的标准,肯定前景会更好,期待会有更好的表现。
这就是asm.js & webassembly与web高性能计算的关系了,之后还有想法写一份结合Rust做WebAssembly的文章,有兴趣的朋友可以持续关注。
以上がasm.js と webassembly-WEB ハイ パフォーマンス コンピューティングの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。