chenshenhai / blog

个人博客,没事写写玩玩~~~
146 stars 21 forks source link

浅尝 WebAssembly 在Node.js和浏览器的性能对比 #38

Open chenshenhai opened 5 years ago

chenshenhai commented 5 years ago

前言

前段时间开发图像处理工具Pictool后,遇到高频的计算瓶颈。在寻找高频计算的前端能力解决方案过程中,入门学习了一下 WebAssembly 在前端中的应用。入门的过程中踩了不少坑,例如使用AssemblyScript 开发wasm时候,发现 npmassemblyscript 已经不维护了,需要自己人工添加成从Github 仓库引用assemblyscriptnpm模块。

同时网上很多教程已经有点不同步,很多按照教程步骤后实现的代码跑不起来。最后参考原有网上的教程,一步步踩坑,实现了demo,同时也写下这篇文章作为笔记!

WebAssembly

什么是 WebAssembly

WebAssembly 优势

在前端主要的优势有

WebAssembly 前端能力现状

什么是 AssemblyScript

快速开始

demo源码地址

如果想更快速尝试,可以直接去该 demo 仓库获取源码使用。

https://github.com/chenshenhai/assemblyscript-demo

安装 AssemblyScript

由于 AssemblyScriptnpm 官方模块已经停止维护,所以AssemblyScript的模块需要从Github 来源安装。

wasm-003

package.json的依赖加入 AssemblyScript 模块的 Github 来源

./package.json

{
  // ...
  "devDependencies": {
    "assemblyscript": "github:assemblyscript/assemblyscript"
    // ...
  }
}

再执行 npm installGithub 下载该模块到本地 node_module

npm install

编写功能代码

编写一个 斐波那契数列 函数

在 demo 的目录 ./src/index.ts

export function fib(num: i32): i32 {
  if (num === 1 || num === 2) {
    return 1;
  } else {
    return fib(num - 1) + fib(num - 2)
  }
}

编译

package.json 编写编译脚本

./package.json

{
  // ...
  "scripts": {
    "build": "npm run build:untouched && npm run build:optimized",
    "build:untouched": "./node_modules/assemblyscript/bin/asc src/index.ts -t dist/module.untouched.wat -b dist/module.untouched.wasm --validate --sourceMap --measure",
    "build:optimized": "./node_modules/assemblyscript/bin/asc src/index.ts -t dist/module.optimized.wat -b dist/module.optimized.wasm --validate --sourceMap --measure --optimize"

    // ...
  },
}

在项目根目录开始执行编译

npm run build

后面会在 ./dist/ 目录下产生编译后的几种 wasm 文件格式

├── dist
│   ├── module.optimized.wasm
│   ├── module.optimized.wasm.map
│   ├── module.optimized.wat
│   ├── module.untouched.wasm
│   ├── module.untouched.wasm.map
│   └── module.untouched.wat

Node.js 使用

./example/node/module.js 文件中,封装wasmCommonJS使用模块

const fs = require('fs');
const path = require('path');

const wasmFile = fs.readFileSync(path.join(__dirname, '..', '..', './dist/module.optimized.wasm'))

const wasm = new WebAssembly.Module(wasmFile, {});

module.exports = new WebAssembly.Instance(wasm, {
  env: {
    memoryBase: 0,
    tableBase: 0,
    memory: new WebAssembly.Memory({
      initial: 256,
      maximum: 512,
    }),
    table: new WebAssembly.Table({
      initial: 0,
      maximum: 0,
      element: 'anyfunc',
    }),
    abort: console.log,
  },
}).exports;

Node.js 使用

const mod = require('./module');

const result = mod.fib(40);
console.log(result);

执行 Node.js 的 wasm 引用

输出结果会是

102334155

浏览器使用

./example/browser/ 目录下部署浏览器访问的服务

├── dist
│   ├── module.optimized.wasm
│   └── module.untouched.wasm
├── example
│   ├── browser
│   │   ├── demo.js
│   │   ├── index.html
│   │   └── server.js

临时浏览器可访问的服务,这里用 koa 来搭建服务

具体实现在 ./example/browser/server.js 文件中

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

const staticPath = './../../'

app.use(static(
  path.join( __dirname,  staticPath)
))

app.listen(3000, () => {
  console.log('[INFO]: server starting at port 3000');
  console.log('open: http://127.0.0.1:3000/example/browser/index.html')
})

浏览器使用 wasm 模块

具体实现在 ./example/browser/demo.js 文件中实现

const $body = document.querySelector('body');

fetch('/dist/module.optimized.wasm')
  .then(res => res.arrayBuffer())
  .then((wasm) => {
    return new WebAssembly.instantiate(wasm, {
      env: {
        memoryBase: 0,
        tableBase: 0,
        memory: new WebAssembly.Memory({
          initial: 256,
          maximum: 512,
        }),
        table: new WebAssembly.Table({
          initial: 0,
          maximum: 0,
          element: 'anyfunc',
        }),
        abort: console.log,
      },
    })
  }).then(mod => {
    const result = mod.instance.exports.fib(40);
    console.log(result)
  });

访问页面在 ./example/browser/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>demo</title>
</head>
<body>
</body>
<script src="demo.js"></script>
</html>

启动服务

node ./example/browser/server.js

浏览器访问页面

http://127.0.0.1:3000/example/browser/index.html

浏览器会出现结果

102334155

性能测试

Node.js 对比测试

const mod = require('./module');

const start = Date.now();
mod.fib(40)
// 打印 Node.js 环境下 wasm 计算 斐波那契数列 参数为40 的耗时结果
console.log(`nodejs-wasm time consume: ${Date.now() - start} ms`)

// 原生Node.js实现的 斐波那契数列 函数
function pureFib(num) {
  if (num === 1 || num === 2) {
    return 1;
  } else {
    return pureFib(num - 1) + pureFib(num - 2)
  }
}

const startPure = Date.now()
pureFib(40);
// 打印 Nodejs环境下 原生js 计算 斐波那契数列 参数为40 的耗时结果
console.log(`nodejs-js time consume: ${Date.now() - startPure} ms`)

测试结果

wasm-001

浏览器对比测试

const $body = document.querySelector('body');

fetch('/dist/module.optimized.wasm')
  .then(res => res.arrayBuffer())
  .then((wasm) => {
    return new WebAssembly.instantiate(wasm, {
      env: {
        memoryBase: 0,
        tableBase: 0,
        memory: new WebAssembly.Memory({
          initial: 256,
          maximum: 512,
        }),
        table: new WebAssembly.Table({
          initial: 0,
          maximum: 0,
          element: 'anyfunc',
        }),
        abort: console.log,
      },
    })
  }).then(mod => {

    const start = Date.now();
    mod.instance.exports.fib(40);
    const logWasm = `browser-wasm time consume: ${Date.now() - start} ms`;
    $body.innerHTML =  $body.innerHTML + `<p>${logWasm}</p>`
    // 打印 浏览器环境下 wasm 计算 斐波那契数列 参数为40 的耗时结果
    console.log(logWasm)
  });

  // 打印 浏览器环境下 原生js 计算 斐波那契数列 参数为40 的耗时结果
  function pureFib(num) {
    if (num === 1 || num === 2) {
      return 1;
    } else {
      return pureFib(num - 1) + pureFib(num - 2)
    }
  }
  const startPure = Date.now()
  pureFib(40);
  const logPure = `browser-js time consume: ${Date.now() - startPure} ms`;
  $body.innerHTML =  $body.innerHTML + `<p>${logPure}</p>`
  console.log(logPure);

测试结果

wasm-002

从上述 Node.js 和 Chrome 环境下运行 wasm 和 原生js 的对比中,wasm的在高频计算的场景下,耗时的确是比原生js低,同时都是接近 30% 的计算性能提升。

参考资料

ghost commented 4 years ago

npm 上assemblyscript 目前版本是 0.8.1 和 github仓库保持同步。