WebAssembly を使用した JavaScript ライブラリの最適化、失敗した試み!


Rust は、Web 用のツールを構築するための言語としてますます選択されるようになっています.これまでの最後の例、 Rome announced it will be using Rust .

歴史的に、Rust は WebAssembly を対象とする言語の 1 つでもあり、現在ではすべての主要なブラウザーで出荷されています. WebAssembly を使用する主な利点の 1 つは、ほとんどの場合、プレーンな JavaScript コードよりもパフォーマンスが高いことです.したがって、私がリリースした最新のライブラリ ( https://github.com/AntonioVdlC/range ) を Rust で書き直して最適化するというアイデアです!


しかし、まず最初に.非常に頭の良い人がかつて、測定できるものだけを改善できると言いました.先に進む前に、@antoniovdlc/range ライブラリのパフォーマンスを測定する方法を見てみましょう.

Node でベンチマークを実行するための適切なオプションがいくつかあります (たとえば、適切な名前の benchmark ライブラリ、または Parcel で使用される tiny-benchy) が、この演習のために、下位レベルの API を調べて、Node のperf_hooks

#!/usr/bin/env node

const { performance, PerformanceObserver } = require("perf_hooks");

const range = require("../dist/index.cjs");

const testBenchmark = performance.timerify(function testBenchmark() {
  let sum = 0;
  let i = 0;

  const r = range(0, process.env.SIZE);
  while (!r.next().done) {
    sum += i;
    i++;
  }

  return sum;
});

const obs = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const avgDuration =
    entries.reduce((sum, cur) => (sum += cur.duration), 0) / entries.length;

  console.log(`range(0, ${process.env.SIZE}): ${avgDuration}s`);
  obs.disconnect();
});

obs.observe({ entryTypes: ["function"] });

for (let i = 0; i < 1000; i++) {
  testBenchmark();
}


上記のコードが行うことは、指定されたサイズの範囲をループし、各反復で単純な合計演算を行う関数を 1,000 回実行することです.次に、ベンチマークは、これら 1,000 回の実行すべての平均時間として計算されます.

まず、現在の実装のパフォーマンスを見てみましょう.

range(0, 100): 0.007962769627571106s
range(0, 1000): 0.015898147106170653s
range(0, 10000): 0.08853049981594086s
range(0, 100000): 0.8147728093862534s
range(0, 1000000): 7.5012646638154985s


正直、ぼろぼろではありません! Rust と WebAssembly でもっとうまくやれるでしょうか?

If you want to follow along, please make sure to install Rust and its toolchain.



Rust コードを WebAssembly にコンパイルするには、wasm-pack を使用します.

Cargo でインストールするか、npm から直接インストールできます.

npm i -D wasm-pack


次に、次のスクリプトを package.json に追加できます.

{
  ...
  "scripts": {
    ...
    "build:wasm": "wasm-pack build --target nodejs"
  }
}


それでは、Rust コードを書きましょう.

最初に、 Range という構造体を宣言します.これは、 implementation of ranges in JavaScript と非常によく似ています.

#[wasm_bindgen]
pub struct Range {
    _start: i32,
    _stop: i32,
    _step: i32,
    _inclusive: bool,

    // Counter used for iteration, so that we can iterate multiple times over
    // the same range
    i: i32,
}

#[wasm_bindgen]
impl Range {
    #[wasm_bindgen(constructor)]
    pub fn new(start: i32, stop: i32, step: i32, inclusive: bool) -> Range {
        Range {
            _start: start,
            _stop: stop,
            _step: if step != 0 { step } else { 1 },
            _inclusive: inclusive,
            i: start,
        }
    }
}


JavaScript で最初に実装したものと同様の API を表示するために、次の range 関数も記述します.

#[wasm_bindgen]
pub fn range(start: i32, stop: i32, step: i32, inclusive: bool) -> Result<Range, JsValue> {
    if start > stop {
        return Err(Error::new(
            (format!("Cannot create a range from {} to {}", start, stop)).as_str(),
        )
        .into());
    }

    return Ok(Range::new(start, stop, step, inclusive));
}


ゲッターやその他のメソッドを実装することもできますが、この演習にあまり投資する前に、コンパイルされた WebAssembly コードでベンチマークを実行できるように、.next() メソッドの実装に焦点を当てましょう.

#[wasm_bindgen]
pub struct JsIteratorResult {
    pub value: Option<i32>,
    pub done: bool,
}



#[wasm_bindgen]
impl Range {
    #[wasm_bindgen]
    pub fn next(&mut self) -> JsIteratorResult {
        if self._inclusive && self.i <= self._stop || self.i < self._stop {
            let value = self.i;
            self.i = self.i + self._step;

            return JsIteratorResult {
                value: Some(value),
                done: false,
            };
        }

        self.i = self._start;

        return JsIteratorResult {
            value: None,
            done: true,
        };
    }
}


上記の実装は、JavaScript コードと非常によく似ています.

上記のRustコードをWebAssemblyにコンパイルしたら、ベンチマークを見てみましょう...

range(0, 100): 0.018000024318695067s
range(0, 1000): 0.09116293668746948s
range(0, 10000): 2.4152168154716493s
...


...そして残念なことに、がっかりする以上の数字です.

その特定のライブラリの WebAssembly バージョンは桁違いに遅いようです.これはおそらく、私が一般的に Rust と WebAssembly に慣れていないことが原因であり、このようなパフォーマンスの低下の原因をより深く調べる方法は間違いなくありますが、失敗したり、停止したり、次の課題を探したりすることも問題ありません!

これは興味深い実験でした.最終結果は期待したものではありませんでしたが、それでも素晴らしい学習の機会でした!


Rust の完全なコード ベースを調べて調整したい場合は、 https://github.com/AntonioVdlC/range/tree/wasm を参照してください.

たぶん、あなたが私に指摘できる明らかな間違いがいくつかあります!