视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
asm.js&webassembly-WEB的高性能计算
2020-11-27 20:11:08 责编:小采
文档
 之前给大家介绍过WebWorkers-WEB的高性能计算,关于javascript的知识都很有意思,那么今天给大家说一说asm.js & webassembly与WEB高性能计算的关系,之前我们说过要解决高性能计算的两个方法,一个是并发用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获取最新版工具

# 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中

~/emsdk-portable
~/emsdk-portable/clang/fastcomp/build_incoming_/bin
~/emsdk-portable/emscripten/incoming

4. 其他

我在执行的时候碰到报错说LLVM版本不对,后来参考文档配置了LLVM_ROOT变量就好了,如果你没有遇到问题,可以忽略。

LLVM_ROOT = os.path.expanduser(os.getenv('LLVM', '/home/ubuntu/a-path/emscripten-fastcomp/build/bin'))

5. 验证是否安装好

执行emcc -v,如果安装好会出现如下信息:

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_-apple-darwin15.5.0
Thread model: posix
InstalledDir: /Users/magicly/emsdk-portable/clang/fastcomp/build_incoming_/bin
INFO:root:(Emscripten: Running sanity checks)

Hello, WebAssembly!

创建一个文件hello.c:

#include <stdio.h>
int main() {
 printf("Hello, WebAssembly!\n");
 return 0;
}

编译C/C++代码:

emcc hello.c

上述命令会生成一个a.out.js文件,我们可以直接用Node.js执行:

node a.out.js

输出

Hello, WebAssembly!

为了让代码运行在网页里面,执行下面命令会生成hello.html和hello.js两个文件,其中hello.js和a.out.js内容是完全一样的。

emcc hello.c -o hello.html<code>
 
? webasm-study md5 a.out.js
MD5 (a.out.js) = d7397f44f817526a4d0f94bc85e429
? webasm-study md5 hello.js
MD5 (hello.js) = d7397f44f817526a4d0f94bc85e429

然后在浏览器打开hello.html,可以看到页面


前面生成的代码都是asm.js,毕竟Emscripten是人家作者Alon Zakai最早用来生成asm.js的,默认输出asm.js也就不足为奇了。当然,可以通过option生成wasm,会生成三个文件:hello-wasm.html, hello-wasm.js, hello-wasm.wasm。

emcc hello.c -s WASM=1 -o hello-wasm.html

然后浏览器打开hello-wasm.html,发现报错TypeError: Failed to fetch。原因是wasm文件是通过XHR异步加载的,用file:////访问会报错,所以我们需要启一个服务器。

npm install -g serve
serve

然后访问http://localhost:5000/hello-wasm.html,就可以看到正常结果了。

调用C/C++函数

前面的Hello, WebAssembly!都是main函数直接打出来的,而我们使用WebAssembly的目的是为了高性能计算,做法多半是用C/C++实现某个函数进行耗时的计算,然后编译成wasm,暴露给js去调用。

在文件add.c中写如下代码:

#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

emcc -s EXPORTED_FUNCTIONS="['_add']" add.c -o add.js

注意方法名add前必须加_。 然后我们可以在Node.js里面这样使用:

// 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页面使用的话,执行:

emcc -s EXPORTED_FUNCTIONS="['_add']" add.c -o add.html

然后在生成的add.html中加入如下代码:

<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,具体细节可以参看文档。

const cAdd = add_module.cwrap('add', 'number', ['number', 'number']);
console.log(cAdd(2, 3));
console.log(cAdd(2, 4));

定义函数的时候添加EMSCRIPTEN_KEEPALIVE

添加文件add2.c。

#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

同样在add2.html中添加代码:

<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的时候,报错:

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 % 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 % 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的文章,有兴趣的朋友可以持续关注。

下载本文
显示全文
专题