前端高性能计算之一:WebWorkers

2017/10/21 · HTML5 ·
WebWorkers

原文出处: magicly   

最近做一个项目,里面涉及到在前端做大量计算,直接用js跑了一下,大概需要15s的时间,
也就是用户的浏览器会卡死15s,这个完全接受不了。

虽说有V8这样牛逼的引擎,但大家知道js并不适合做CPU密集型的计算,一是因为单线程,二是因为动态语言。我们就从这两个突破口入手,首先搞定“单线程”的限制,尝试用WebWorkers来加速计算。

前端高性能计算之二:asm.js & webassembly

2017/10/21 · HTML5 ·
webassembly

原文出处: magicly   

前一篇我们说了要解决高性能计算的两个方法,一个是并发用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应该是一个前景很好的项目。我们可以看一下目前浏览器的支持情况:
图片 1

认识Web Worker

  1. Web Worker是
    运行在后台的javascript,也就是说worker其实就是就一个js文件对象,worker可以让他所包含的js代码运行在后台
  • 特点:

    • 充分利用多核CPU的优势
    • 对多线程支持非常好
    • 不会影响页面的性能
    • 不能访问web页面和DOM API
    • 所有的主流浏览器均支持web worker,除了Internet Explorer
  1. Worker提供API

    1)检测当前浏览器是否支持Worker

    typeof(Worker) !== "undefined“ 
    

    2)创建Worker文件
    创建普通的 JS 文件,都可以用于 Web Worker 文件

    3)创建Web Worker对象

      var worker = new Worker("myTime.js");
    

参数就是在第二步创建的js文件的路径

  4)worker事件

* onmessage事件

用于监听 Web Worker
传递消息,通过回调函数接收传递的消息,通过回调函数的事件对象data
属性可以获取传递的消息

  • postMessage()

       w.postMessage( “worker success.” );
    

通过postMessage() 方法传递消息内容

     w.terminate();

在HTML页面中,通过调用 Web Worker 对象的terminate( ) 方法终止 Web
Worker。

流程:
  • 创建WebWorker对象
  • Worker对象
  • Worder事件
    * onmessage事件,当执行postMessage事件时会触发
    * postMessage()方法
    * terminate()方法

什么是WebWorkers

简单说,WebWorkers是一个HTML5的新API,web开发者可以通过此API在后台运行一个脚本而不阻塞UI,可以用来做需要大量计算的事情,充分利用CPU多核。

大家可以看看这篇文章介绍https://www.html5rocks.com/en/tutorials/workers/basics/,
或者对应的中文版。

The Web Workers specification defines an API for spawning background
scripts in your web application. Web Workers allow you to do things
like fire up long-running scripts to handle computationally intensive
tasks, but without blocking the UI or other scripts to handle user
interactions.

可以打开这个链接自己体验一下WebWorkers的加速效果。

现在浏览器基本都支持WebWorkers了。
图片 2

安装Emscripten

访问

  1. 下载对应平台版本的SDK

  2. 通过emsdk获取最新版工具

JavaScript

# 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

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
  1. 将下列添加到环境变量PATH中

JavaScript

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

1
2
3
~/emsdk-portable
~/emsdk-portable/clang/fastcomp/build_incoming_64/bin
~/emsdk-portable/emscripten/incoming
  1. 其他

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

JavaScript

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

1
LLVM_ROOT = os.path.expanduser(os.getenv(‘LLVM’, ‘/home/ubuntu/a-path/emscripten-fastcomp/build/bin’))
  1. 验证是否安装好

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

JavaScript

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld)
1.37.21 clang version 4.0.0
(
974b55fd84ca447c4297fc3b00cefb6394571d18)
(
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)

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)

Parallel.js

直接使用WebWorkers接口还是太繁琐,好在有人已经对此作了封装:Parallel.js。

注意Parallel.js可以通过node安装:

$ npm install paralleljs

1
$ npm install paralleljs

不过这个是在node.js下用的,用的node的cluster模块。如果要在浏览器里使用的话,
需要直接应用js:

<script src=”parallel.js”></script>

1
<script src="parallel.js"></script>

然后可以得到一个全局变量,ParallelParallel提供了mapreduce两个函数式编程的接口,可以非常方便的进行并发操作。

我们先来定义一下我们的问题,由于业务比较复杂,我这里把问题简化成求1-1,0000,0000的和,然后在依次减去1-1,0000,0000,答案显而易见:
0!
这样做是因为数字太大的话会有数据精度的问题,两种方法的结果会有一些差异,会让人觉得并行的方法不可靠。此问题在我的mac
pro
chrome61下直接简单地跑js运行的话大概是1.5s(我们实际业务问题需要15s,这里为了避免用户测试的时候把浏览器搞死,我们简化了问题)。

const N = 100000000;// 总次数1亿 function sum(start, end) { let total =
0; for (let i = start; i<=end; i += 1) total += i; for (let i =
start; i<=end; i += 1) total -= i; return total; } function
paraSum(N) { const N1 = N / 10;//我们分成10分,没分分别交给一个web
worker,parallel.js会根据电脑的CPU核数建立适量的workers let p = new
Parallel([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) .require(sum); return
p.map(n => sum((n – 1) * 10000000 + 1, n * 10000000))//
在parallel.js里面没法直接应用外部变量N1 .reduce(data => { const acc =
data[0]; const e = data[1]; return acc + e; }); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const N = 100000000;// 总次数1亿
 
function sum(start, end) {
  let total = 0;
  for (let i = start; i<=end; i += 1) total += i;
  for (let i = start; i<=end; i += 1) total -= i;
  return total;
}
 
function paraSum(N) {
  const N1 = N / 10;//我们分成10分,没分分别交给一个web worker,parallel.js会根据电脑的CPU核数建立适量的workers
  let p = new Parallel([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    .require(sum);
  return p.map(n => sum((n – 1) * 10000000 + 1, n * 10000000))// 在parallel.js里面没法直接应用外部变量N1
    .reduce(data => {
      const acc = data[0];
      const e = data[1];
      return acc + e;
    });
}

代码比较简单,我这里说几个刚用的时候遇到的坑。

  • require所有需要的函数

比如在上诉代码中用到了sum,你需要提前require(sum),如果sum中由用到了另一个函数f,你还需要require(f),同样如果f中用到了g,则还需要require(g),直到你require了所有用到的定义的函数。。。。

  • 没法require变量

我们上诉代码我本来定义了N1,但是没法用

  • ES6编译成ES5之后的问题以及Chrome没报错

实际项目中一开始我们用到了ES6的特性:数组解构。本来这是很简单的特性,现在大部分浏览器都已经支持了,不过我当时配置的babel会编译成ES5,所以会生成代码_slicedToArray,大家可以在线上Babel测试,然后Chrome下面始终不work,也没有任何报错信息,查了很久,后来在Firefox下打开,有报错信息:

ReferenceError: _slicedToArray is not defined

1
ReferenceError: _slicedToArray is not defined

看来Chrome也不是万能的啊。。。

大家可以在此Demo页面测试,
提速大概在4倍左右,当然还是得看自己电脑CPU的核数。
另外我后来在同样的电脑上Firefox55.0.3(64位)测试,上诉代码居然只要190ms!!!在Safari9.1.1下也是190ms左右。。。

Hello, WebAssembly!

创建一个文件hello.c

JavaScript

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

1
2
3
4
5
#include <stdio.h>
int main() {
  printf("Hello, WebAssembly!n");
  return 0;
}

编译C/C++代码:

JavaScript

emcc hello.c

1
emcc hello.c

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

JavaScript

node a.out.js

1
node a.out.js

输出

JavaScript

Hello, WebAssembly!

1
Hello, WebAssembly!

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

emcc hello.c -o hello.html<code>

1
2
emcc hello.c -o hello.html<code>
 

JavaScript

➜ webasm-study md5 a.out.js MD5 (a.out.js) =
d7397f44f817526a4d0f94bc85e46429 ➜ webasm-study md5 hello.js MD5
(hello.js) = d7397f44f817526a4d0f94bc85e46429

1
2
3
4
➜  webasm-study md5 a.out.js
MD5 (a.out.js) = d7397f44f817526a4d0f94bc85e46429
➜  webasm-study md5 hello.js
MD5 (hello.js) = d7397f44f817526a4d0f94bc85e46429

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

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

JavaScript

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

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

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

JavaScript

npm install -g serve serve

1
2
npm install -g serve
serve

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注