为什么我的 WebAssembly 函数比 JavaScript 等效函数慢?

Why is my WebAssembly function slower than the JavaScript equivalent?

对于宽泛的问题深表歉意!我正在学习 WASM 并在 C:

中创建了一个 Mandelbrot 算法
int iterateEquation(float x0, float y0, int maxiterations) {
  float a = 0, b = 0, rx = 0, ry = 0;
  int iterations = 0;
  while (iterations < maxiterations && (rx * rx + ry * ry <= 4.0)) {
    rx = a * a - b * b + x0;
    ry = 2.0 * a * b + y0;
    a = rx;
    b = ry;
    iterations++;
  }
  return iterations;
}

void mandelbrot(int *buf, float width, float height) {
  for(float x = 0.0; x < width; x++) {
    for(float y = 0.0; y < height; y++) {
      // map to mandelbrot coordinates
      float cx = (x - 150.0) / 100.0;
      float cy = (y - 75.0) / 100.0;
      int iterations = iterateEquation(cx, cy, 1000);
      int loc = ((x + y * width) * 4);
      // set the red and alpha components
      *(buf + loc) = iterations > 100 ? 255 : 0;
      *(buf + (loc+3)) = 255;
    }
  }
}

我正在编译为 WASM 如下(为清楚起见省略了文件名输入/输出)

clang -emit-llvm  -O3 --target=wasm32 ...
llc -march=wasm32 -filetype=asm ...
s2wasm --initial-memory 6553600 ...
wat2wasm ... 

我在JavaScript中加载,编译,然后调用如下:

instance.exports.mandelbrot(0, 300, 150)

正在将输出复制到 canvas,这使我能够验证它是否正确执行。在我的电脑上执行上述函数大约需要 120 毫秒。

但是,这里有一个 JavaScript 等价物:

const iterateEquation = (x0, y0, maxiterations) => {
  let a = 0, b = 0, rx = 0, ry = 0;
  let iterations = 0;
  while (iterations < maxiterations && (rx * rx + ry * ry <= 4)) {
    rx = a * a - b * b + x0;
    ry = 2 * a * b + y0;
    a = rx;
    b = ry;
    iterations++;
  }
  return iterations;
}

const mandelbrot = (data) => {
  for (var x = 0; x < 300; x++) {
    for (var y = 0; y < 150; y++) {
      const cx = (x - 150) / 100;
      const cy = (y - 75) / 100;
      const res = iterateEquation(cx, cy, 1000);
      const idx = (x + y * 300) * 4;
      data[idx] = res > 100 ? 255 : 0;
      data[idx+3] = 255;
    }
  }
}

执行仅需 ~62 毫秒。

现在我知道 WebAssembly 是非常新的,并且没有得到很好的优化。但是我不禁觉得应该比这个快!

任何人都可以发现我可能错过的明显内容吗?

此外,我的 C 代码直接写入从“0”开始的内存 - 我想知道这是否安全?堆栈存储在分页线性内存中的什么位置?我要冒险覆盖它吗?

这里有一个fiddle来说明:

https://wasdk.github.io/WasmFiddle/?jvoh5

当 运行 时,它记录两个等效实现的时间(WASM 然后 JavaScript)

一般

与优化的 JS 相比,通常您可以希望在繁重的数学运算上获得约 10% 的提升。其中包括:

  • wasm 利润
  • in/out 内存拷贝开销。

注意,Uint8Array 复制在 chrome 中特别慢(在 FF 中正常)。当您使用 rgba 数据时,最好将底层缓冲区重新转换为 Uint32Array 蚂蚁,并在其上使用 .set()

尝试在 wasm 中按单词 (rgba) read/write 像素的工作速度与 read/write 字节 (r, g, b, a) 相同。我没有发现不同。

当使用 node.js 进行开发时(就像我一样),值得留在 8.2.1 上进行 JS 基准测试。下一个版本将 v8 升级到 v6.0 并为此类数学引入了严重的速度回归。对于 8.2.1 - 不要使用 const=> 等现代 ES6 功能。改用 ES5。 v8 v6.2 的下一个版本可能会解决这些问题。

示例评论

  1. 使用 wasm-opt -O3,这可能会在 clang -O3 之后的某个时候有所帮助。
  2. 使用s2wasm --import-memory而不是硬编码固定内存大小
  3. 在 wasdk 站点的代码中,不要使用全局变量。当它们存在时,编译器将在内存开始处为全局变量分配未知块,您可以错误地覆盖它们。
  4. 可能,正确的代码应该从适当的位置添加内存副本,并且应该包含在基准测试中。您的样本不完整,来自 wasdk 的恕我直言代码应该无法正常工作。
  5. 使用benchmark.js,更精确。

简而言之:在继续之前,清理一些东西是值得的。

您可能会发现挖掘 https://github.com/nodeca/multimath 资源很有用,或者在您的实验中使用它。我专门为小型 CPU 密集型事物创建了它,以简化适当模块初始化、内存管理、js 回退等问题。它包含 'unsharp mask' 实施作为示例和基准。在那里采用你的代码应该不难。

我遇到过 webassembly 运行缓慢的情况。它变成了编译时启用的 SAFE_HEAP 选项。去掉选项后,速度是原生的两倍左右,所以编译选项也是要看的。

Google Chromes 的“检查”window 似乎使 WebAssembly 减慢了约 100%。因此,对于基准测试,您应该使用“警报”而不是使用“console.log”来显示结果。或者,在 MacOS Safari 中进行基准测试,这似乎不会降低 WebAssembly 的速度。 (我还没有尝试过 MS Edge。)

将 WebStorm 等外部调试器链接到 Chrome 也会降低速度。