https://developer.mozilla.org/ja/docs/WebAssembly/Rust_to_wasm に書いてある通りで便利。alert 出しても面白くないのでFFT のベンチをとってみるというのをやってみた。

こういう感じ

タスクは N=4096 の複素数のFFTをやることとした。rust 側のコードは rustfftを呼ぶだけ。

以下のような組合せで実行

  • [wasm] instance wasm_bindgen
    • wasm_bindgen が生成した struct のJSブリッジを使って普通にAPIを呼ぶ
      • API呼び出し時に必ずJSからWASMにTypedArray のコピーが発生する
  • [wasm] instance pointer
    • wasm_bindgen が生成した wasm を直接使って struct のポインタ、バッファのポインタを自力で管理する
    • WASMからメモリを確保して直接使っているのでコピーが減る
  • [wasm] one func pointer, one func wasm_bindgen
    • rust 側の FFT インスタンスを使い捨てるバージョン。あまり意味はないが事前計算がどれぐらい重いのかわかる。
  • [js] instance dsp.js
    • dsp.js の FFT インスタンスを使う場合。厳密には入力・出力が普通のFFTと違うので、参考程度 (入力が実数だけコピーもしてない、出力の振幅を余計に計算しているなど)
N = 4096
[wasm] instance pointer x 8,361 ops/sec ±0.23% (97 runs sampled)
[wasm] instance wasm_bindgen x 7,869 ops/sec ±0.23% (99 runs sampled)
[wasm] one func pointer x 2,730 ops/sec ±0.48% (95 runs sampled)
[wasm] one func wasm_bindgen x 2,753 ops/sec ±0.34% (97 runs sampled)
[js] instance dsp.js x 3,722 ops/sec ±0.74% (92 runs sampled)
Fastest is [wasm] instance pointer
+------------------------------+--------------------------+----------------------------+---------------------------------+-------------------------+---------------------------------+----------------------------+
| name                         | ops                      | vs [wasm] instance pointer | vs [wasm] instance wasm_bindgen | vs [js] instance dsp.js | vs [wasm] one func wasm_bindgen | vs [wasm] one func pointer |
+------------------------------+--------------------------+----------------------------+---------------------------------+-------------------------+---------------------------------+----------------------------+
| [wasm] instance pointer      | 8360.9ops/sec (+/-0.23%) | -                          | 6%                              | 125%                    | 204%                            | 206%                       |
+------------------------------+--------------------------+----------------------------+---------------------------------+-------------------------+---------------------------------+----------------------------+
| [wasm] instance wasm_bindgen | 7869.3ops/sec (+/-0.23%) | -6%                        | -                               | 111%                    | 186%                            | 188%                       |
+------------------------------+--------------------------+----------------------------+---------------------------------+-------------------------+---------------------------------+----------------------------+
| [js] instance dsp.js         | 3721.9ops/sec (+/-0.74%) | -55%                       | -53%                            | -                       | 35%                             | 36%                        |
+------------------------------+--------------------------+----------------------------+---------------------------------+-------------------------+---------------------------------+----------------------------+
| [wasm] one func wasm_bindgen | 2752.8ops/sec (+/-0.34%) | -67%                       | -65%                            | -26%                    | -                               | 1%                         |
+------------------------------+--------------------------+----------------------------+---------------------------------+-------------------------+---------------------------------+----------------------------+
| [wasm] one func pointer      | 2730.0ops/sec (+/-0.48%) | -67%                       | -65%                            | -27%                    | -1%                             | -                          |
+------------------------------+--------------------------+----------------------------+---------------------------------+-------------------------+---------------------------------+----------------------------+

https://github.com/cho45/wasm-fft-sketch/blob/master/sketch.js#L174

とりあえず何も考えずに pure rust のライブラリをコンパイルして呼んでいるだけなのに、wasm 版が早い (rustfft が良いのかもしれないが)。

計算の比重が高いからか、意外とメモリコピーしてていても差がでない。

wasm_bindgen の使い勝手がいい

wasm_bindgen と wasm-pack が大変使い勝手が良く、ほぼ悩むことなく即 Rust のコードを書きはじめて、またそれをすぐに JS から呼ぶことができる。内部的には Rust 側のブリッジ関数と JS 側のブリッジ関数を同時に作ってくれている。

少し効率的な実装に置き換える

ただ、wasm はメモリ空間が JS のメモリ空間と分かれているため、wasm_bindgenが生成するJS側のブリッジ関数は (便利ではあるが) 若干非効率な実装になっており、TypedArray の受け渡しではコピーが多くなる。

これを防ぐには、やはり自力で wasm 側のメモリ空間からメモリを確保して TypedArray をインスタンス化して使用し、必要なくなったら free するという、メモリ管理を自分でやる必要がある。

これはまぁまぁ面倒くさいが、生成されたJSコードを読めばどう呼べば適切かは容易にわかるので、とりあえずは難しいことではない。

メモリ管理は必要

生成コードをただ使う場合、関数呼び出しだけなら生成コード内でメモリのfreeが行われるので、あまり気にする必要はないが、struct に関しては生成コードをただ使っている場合でも、明示的にオブジェクトの free を呼ぶ必要がある。JS にはデストラクタがないので仕方ないが、注意がいる。

  1. トップ
  2. tech
  3. Rust + wasm の環境が wasm_bindgen でめっちゃ簡単になっていた

Benchmark.js ちゃんと使えるので良いのですが、計測を頑張っている割に結果表示が貧弱というのが悲しいところです。

なので Perl の Benchmark.pm 風に表示する complete の関数を書いてみました。cli-table に依存します。

for x 115,102,309 ops/sec ±0.43% (95 runs sampled)
for of x 62,020,029 ops/sec ±0.23% (96 runs sampled)
Fastest is for
+--------+-------------------------------+--------+-----------+
| name   | ops                           | vs for | vs for of |
+--------+-------------------------------+--------+-----------+
| for    | 115102308.6ops/sec (+/-0.43%) | -      | 86%       |
+--------+-------------------------------+--------+-----------+
| for of | 62020028.7ops/sec (+/-0.23%)  | -46%   | -         |
+--------+-------------------------------+--------+-----------+
//#!/usr/bin/env node

"use strict";

const Benchmark = require('benchmark');
const Table = require('cli-table');
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
new Benchmark.Suite().
	add('for', () => {
		let sum = 0;
		for (let i = 0, len = array.length; i < len; i++) {
			sum += array[i];
		}
		return sum;
	}).
	add('for of', () => {
		let sum = 0;
		for (let i of array) {
			sum += i;
		}
		return sum;
	}).
	on('cycle', function(event) {
		console.log(String(event.target));
	}).
	on('complete', function() {
		console.log('Fastest is ' + this.filter('fastest').map('name'));

		const array = this.slice(0).sort( (a, b) => b.hz - a.hz);
		const table = new Table({
			chars: {
				'top': '-' ,
				'top-mid': '+' ,
				'top-left': '+' ,
				'top-right': '+',
				'bottom': '-' ,
				'bottom-mid': '+' ,
				'bottom-left': '+' ,
				'bottom-right': '+',
				'left': '|' ,
				'left-mid': '+' ,
				'mid': '-' ,
				'mid-mid': '+',
				'right': '|' ,
				'right-mid': '+' ,
				'middle': '|'
			},
			head: ['name', 'ops'].concat( array.map( b => 'vs ' + b.name ) )
		});
		const comparison = array.map( (a, ia) => array.map( (b, ib) => {
			if (ia === ib) return "-";
			return Math.round((a.hz / b.hz - 1) * 100) + '%';
		}));
		array.forEach( (bench, i) => {
			table.push([
				bench.name,
				`${bench.hz.toFixed(1)}ops/sec (+/-${bench.stats.rme.toFixed(2)}%)`
			].concat(comparison[i]))
		});
		console.log(table.toString());
	}).
	run({});
  1. トップ
  2. tech
  3. Benchmark.js の結果表示を改善する

input channel 数はちゃんと2なのに同じデータが入っている

モノラルになるとかじゃなく、ステレオ入力すると、デフォルトではモノラル結合されて、同じデータが2チャンネルにコピーされて流れてくる。

解決方法

echoCancelation が有効だとこういう挙動になるらしく、これを止めさせるとちゃんとステレオデータがとれる。getUserMedia で以下のように指定する。余計なことをできるだけ止めさせたい場合はいろいろ指定する必要がある。

const stream = await navigator.mediaDevices.getUserMedia({
        audio: {
                channelCount: {ideal: 2, min: 1},
                echoCancellation: { exact: false },
                noiseSuppression: { exact: false },
                autoGainControl:{ exact:  false },  
        }
});

ref

  1. トップ
  2. tech
  3. Chrome の WebAudio でステレオ入力ができない場合