>  기사  >  백엔드 개발  >  프런트엔드 고성능 컴퓨팅 공유 실현

프런트엔드 고성능 컴퓨팅 공유 실현

小云云
小云云원래의
2017-12-05 10:20:222345검색

프런트엔드 고성능 컴퓨팅 중 하나: WebWorkers WebWorkers란

간단히 말하면 WebWorkers는 HTML5의 새로운 API로, 웹 개발자는 이 API를 사용하여 UI를 차단하지 않고 백그라운드에서 스크립트를 실행할 수 있습니다. 필요한 작업을 수행하는 데 사용할 수 있습니다. 계산량이 많은 경우 여러 CPU 코어를 활용하세요.

기본적으로 모든 브라우저는 이제 WebWorkers를 지원합니다.

Parallel.js

WebWorkers 인터페이스를 직접 사용하는 것은 여전히 ​​너무 번거롭습니다. 다행히도 누군가 Parallel.js로 이미 캡슐화했습니다.

Parallel.js는 node를 통해 설치할 수 있습니다.

$ npm install paralleljs

그러나 이는 node.js에서 node의 클러스터 모듈을 사용하여 사용됩니다. 브라우저에서 사용하려면 js를 직접 적용해야 합니다:

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

그런 다음 전역 변수인 Parallel을 얻을 수 있습니다. Parallel은 두 가지 기능적 프로그래밍 인터페이스인 map과 Reduce를 제공하여 동시 작업을 매우 편리하게 만듭니다.

먼저 문제를 정의해 보겠습니다. 사업이 상대적으로 복잡하기 때문에 여기서는 문제를 단순화하여 1~1,000,0000의 합을 구한 다음 1~1,000,0000을 차례로 빼겠습니다. 명백하다: 0 ! 숫자가 너무 크면 데이터 정확성에 문제가 생기고 두 방법의 결과가 다소 달라서 사람들이 병렬 방식을 신뢰할 수 없다고 느낄 수 있기 때문입니다. Mac pro chrome61에서 js를 직접 실행하면 이 문제는 약 1.5초가 걸립니다(실제 비즈니스 문제는 15초가 걸립니다. 사용자 테스트 중에 브라우저가 종료되는 것을 방지하기 위해 문제를 단순화했습니다).

const N = 100000000;// 总次数1亿

// 更新自2017-10-24 16:47:00
// 代码没有任何含义,纯粹是为了模拟一个耗时计算,直接用
//   for (let i = start; i <= end; i += 1) total += i;
// 有几个问题,一是代码太简单没有任何稍微复杂一点的操作,后面用C代码优化的时候会优化得很夸张,没法对比。
// 二是数据溢出问题, 我懒得处理这个问题,下面代码简单地先加起来,然后再减掉,答案显而易见为0,便于测试。
function sum(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 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;
    });
}

export { N, sum, paraSum }

코드는 비교적 간단합니다. 여기서는 처음 사용했을 때 겪었던 몇 가지 함정에 대해 이야기하겠습니다.

require all 필수 함수

예를 들어 sum이 어필 코드에 사용된다면 sum이 다른 함수 f를 사용하는 경우에도 require(f)를 미리 해야 합니다. if f g를 사용하는 경우 정의된 모든 함수가 사용되도록 요구할 때까지 require(g)도 필요합니다. . . .

변수를 요구할 수 없습니다

저희 어필 코드에는 원래 N1을 정의했는데 사용할 수 없었습니다

ES6를 ES5로 컴파일한 후 Chrome에서 오류가 보고되지 않는 문제

에서 실제 프로젝트에서는 처음에 ES6를 사용했습니다. 기능: 배열 구조 분해. 원래는 아주 간단한 기능이었지만 지금은 대부분의 브라우저에서 지원하지만, 당시 제가 구성한 바벨은 ES5로 컴파일이 되기 때문에 바벨로 온라인에서 테스트할 수는 없을 것입니다. Chrome에서 작동합니다. 오류 메시지가 없었습니다. 오랫동안 확인한 후 Firefox에서 열었더니

ReferenceError: _slicedToArray is not defined

Chrome이 전능하지 않은 것 같습니다. . .

이 데모 페이지에서 테스트할 수 있습니다. 물론 속도 증가는 컴퓨터 CPU의 코어 수에 따라 다릅니다. 또한 나중에 동일한 컴퓨터에서 Firefox 55.0.3(64비트)을 테스트했는데 실제로 항소 코드가 190ms밖에 걸리지 않았습니다! ! ! Safari9.1.1에서도 약 190ms입니다. . .

Refers

  • https://developer.mozilla.org/en/docs/Web/API/WebWorkersAPI/Usingwebworkers

  • https://www.html5rocks.com/en/tutorials/workers/basics/

  • https://parallel.js.org/

  • https://johnresig.com/blog/web-workers/

  • http://javascript.ruanyifeng.com/htmlapi/ webworker. html

  • http://blog.teamtreehouse.com/using-web-workers-to-speed-up-your-javascript-applications

프런트엔드 고성능 컴퓨팅 2부: asm.js & webassemble

앞서 고성능 컴퓨팅을 해결하는 두 가지 방법에 대해 이야기했는데, 하나는 WebWorkers를 동시에 사용하는 것이고, 다른 하나는 저수준 정적 언어를 사용하는 것입니다.

2012년 Mozilla 엔지니어 Alon Zakai는 LLVM 컴파일러를 연구하던 중 갑자기 아이디어를 얻었습니다. C/C++를 Javascript로 컴파일하여 네이티브 코드의 속도를 달성할 수 있을까? 그래서 그는 C/C++ 코드를 Javascript의 하위 집합인 asm.js로 컴파일하는 데 사용되는 Emscripten 컴파일러를 개발했습니다. 성능은 네이티브 코드의 거의 50%입니다. 이 PPT를 보시면 됩니다.

나중에 Google은 브라우저에서 C/C++ 코드를 실행할 수 있는 기술이기도 한 [Portable Native Client][PNaCI]를 개발했습니다. 나중에는 모두가 자신의 일을 하는 것이 불가능하다고 생각한 것 같습니다. 실제로 Google, Microsoft, Mozilla, Apple 및 기타 주요 회사가 협력하여 웹용 범용 바이너리 및 텍스트 형식 프로젝트를 개발했습니다. 공식 웹사이트는

Quote

WebAssembly 또는 wasm은 웹 컴파일에 적합한 새로운 이식 가능하고 크기 및 로드 시간 효율적인 형식입니다.

그래서 WebAssembly는 전망이 좋은 프로젝트여야 합니다. 현재 브라우저 지원을 살펴볼 수 있습니다:

Emscripten 설치

https://kripken.github.io/emscripten-site/docs/getting_started/downloads.html

1을 방문하세요. 버전 SDK

2. emsdk

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

 

➜  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=&#39;text/javascript&#39;>
    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=&#39;text/javascript&#39;>
    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)

 

所以建议采用第一种方法。

上述生成的代码都是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=&#39;text/javascript&#39;>
    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=&#39;text/javascript&#39;>
    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

 

Browser webassembly asm.js js
Chrome61 1300ms 600ms 3300ms
Firefox55 600ms 800ms 700ms
Safari9.1 不支持 2800ms 因不支持ES6我懒得改写没测试

感觉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

 

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

Refers

人工智能是最近两年绝对的热点,而这次人工智能的复兴,有一个很重要的原因就是计算能力的提升,主要依赖于GPU。去年Nvidia的股价飙升了几倍,市面上好点的GPU一般都买不到,因为全被做深度学习以及挖比特币的人买光了

以上内容就是实现前端高性能计算分享,希望能帮助到大家。

相关推荐:

最流行的几个Web前端框架

WebWorkers-前端的高性能计算

web前端知识体系总结

위 내용은 프런트엔드 고성능 컴퓨팅 공유 실현의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.