前端高性能计算(2):asm.js & webassembly

2017 年 10 月 24 日 前端大全

(点击上方公众号,可快速关注)

作者:magicly

https://magicly.me/fe-hpc/asmjs-and-webassembly

如有好文章投稿,请点击 → 这里了解详情



前一篇我们说了要解决高性能计算的两个方法,一个是并发用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_64/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_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:


#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) = d7397f44f817526a4d0f94bc85e46429

  webasm-study md5 hello.js

MD5 (hello.js) = d7397f44f817526a4d0f94bc85e46429


然后在浏览器打开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 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受到各大厂商一致推崇,作为一个新的标准,肯定前景会更好,期待会有更好的表现。


Rust


本来还想写Rust编译成WebAssembly的,不过感觉本文已经太长了, 后期再写如果结合Rust做WebAssembly吧。


着急的可以先看看这两篇


  • Compiling to the web with Rust and emscripten(https://users.rust-lang.org/t/compiling-to-the-web-with-rust-and-emscripten/7627)

  • Rust ⇋ JavaScript(https://www.slideshare.net/RReverser/rust-javascript)


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



觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

登录查看更多
1

相关内容

超文本标记语言(英文:HyperText Markup Language,HTML)是为“网页创建和其它可在网页浏览器中看到的信息”设计的一种标记语言。
CVPR 2020 最佳论文与最佳学生论文!
专知会员服务
35+阅读 · 2020年6月17日
【SIGMOD2020-腾讯】Web规模本体可扩展构建
专知会员服务
29+阅读 · 2020年4月12日
《动手学深度学习》(Dive into Deep Learning)PyTorch实现
专知会员服务
119+阅读 · 2019年12月31日
[综述]深度学习下的场景文本检测与识别
专知会员服务
77+阅读 · 2019年10月10日
机器学习相关资源(框架、库、软件)大列表
专知会员服务
39+阅读 · 2019年10月9日
微信小程序支持webP的WebAssembly方案
前端之巅
19+阅读 · 2019年8月14日
通过Docker安装谷歌足球游戏环境
CreateAMind
11+阅读 · 2019年7月7日
谷歌足球游戏环境使用介绍
CreateAMind
33+阅读 · 2019年6月27日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
Neo4j 和图数据库起步
Linux中国
8+阅读 · 2017年12月20日
前端高性能计算(4):GPU加速计算
前端大全
7+阅读 · 2017年10月26日
Arxiv
24+阅读 · 2020年3月11日
Real-time Scalable Dense Surfel Mapping
Arxiv
5+阅读 · 2019年9月10日
Factor Graph Attention
Arxiv
6+阅读 · 2019年4月11日
Arxiv
11+阅读 · 2018年9月28日
Arxiv
17+阅读 · 2018年4月2日
VIP会员
相关资讯
微信小程序支持webP的WebAssembly方案
前端之巅
19+阅读 · 2019年8月14日
通过Docker安装谷歌足球游戏环境
CreateAMind
11+阅读 · 2019年7月7日
谷歌足球游戏环境使用介绍
CreateAMind
33+阅读 · 2019年6月27日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
Neo4j 和图数据库起步
Linux中国
8+阅读 · 2017年12月20日
前端高性能计算(4):GPU加速计算
前端大全
7+阅读 · 2017年10月26日
Top
微信扫码咨询专知VIP会员