皇上,还记得我吗?我就是1999年那个Linux伊甸园啊-----24小时滚动更新开源资讯,全年无休!

前端高性能计算之二:asm.js & 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 应该是一个前景很好的项目。我们可以看一下 目前浏览器的支持情况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

转自 http://web.jobbole.com/92796/

分享到:更多 ()