😺

Web Worker+WebAssemblyでマルチスレッド: Rustが征く(10)

2021/12/20に公開

Wasm+WebWorkerは使い方が限定的

関連記事:

Rustが征くシリーズ過去記事

------------------- ↓ 前書はここから ↓-------------------

前回の記事で、
JSのマルチスレッドのポテンシャルを測った。
環境によりけりだろうけど、
大体シングルスレッドの倍の速度が出ることがわかったので、
WebAssemblyを使うとどのくらい変わるのかを試す。

WebAssemblyでマルチスレッドを実装するときに使うモジュールは、
結局のところJavaScriptのWorkerファイルを生成して、
JS経由でWebWorkerを使う流れになるので、
WebAssemblyでマルチスレッド と言えるかどうか微妙なところ。
(まだ全部を試せてないけど。とにかく動かすだけで大変なのよねぇ)

さて、ここでは各種マルチスレッドモジュールを使うのは一端おいといて、
最小構成のWebWorker+WebAssemblyを実装してみる。
バンドラーも今回は使用しない。
(workerファイルの扱いが難しい)
モジュールは一つ一つ試す感じにしよう

ヾ(・ω<)ノ" 三三三● ⅱⅲ コロコロ♪

------------------- ↓ 本題はここから ↓-------------------

インストール

必要なコマンドをインストール

cargo install cargo-edit wasm-pack wasm-bindgen-cli

プロジェクト作成

wasm-packのテンプレートを使ってプロジェクトを作成。
ディレクトリ生成もかねて仮ビルド

wasm-pack new wasm-worker
cd wasm-worker
wasm-pack build --release --target no-modules --out-dir ./dist/pkg --out-name wasm-worker

フロントエンドを設置

各種フロントエンドを準備

./dist/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>
  <script type="module" src="./js/index.mjs"></script>
</body>
</html>

ワーカーを2つ生成

./dist/js/index.mjs
(async () => {
const maxWorkers = 2
const workers = []

for (let i = 0; i < maxWorkers; i++) {
    workers[i] = new Worker('/js/worker.cjs')
    workers[i].addEventListener('message', (event) => {
        console.log("Data from worker"+i+" received: ", event.data);
    }, false);
}
})()

workerファイルを設置

Workerに読み込ませる独立したファイルを設置

./dist/js/worker.cjs
importScripts("/pkg/wasm-worker.js")
const { greet, add, fibonacci } = wasm_bindgen;

(async () => {
  await wasm_bindgen("/pkg/wasm-worker_bg.wasm")

  console.log(greet("dozo"))
  console.log(add(2,5),fibonacci(30))
})();

実装してビルド

テンプレートにはalert表示の関数が書いてあるが、
全部消して以下に書き換える。
文字列やりとりと足し算とフィボナッチ数列計算をさせてみる

./src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(s: &str) -> String {
  return format!("Hello {}!", s);
}

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
  return a + b;
}

#[wasm_bindgen]
pub fn fibonacci(num: i32) -> i32 {
  return match num {
    0 => 0,
    1 => 1,
    _ => fibonacci(num - 1) + fibonacci(num - 2),
  };
}

ビルド開始

wasm-pack build --release --target no-modules --out-dir ./dist/pkg --out-name wasm-worker

サーバーファイルを設置

WebAssemblyのMIMETypeとクロスオリジン問題を考慮して、
expressで頑張る系のサーバーファイルを用意

./server.cjs
const express = require('express')
const app = express()
const port = 3000

express.static.mime.define({'application/wasm': ['wasm']})

app.use(express.static('./dist', {
  setHeaders: (res) => {
    res.set('Cross-Origin-Opener-Policy', 'same-origin');
    res.set('Cross-Origin-Embedder-Policy', 'require-corp');
  }
}));

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

サーバー起動

http://localhost:3000 にアクセス

node server.cjs

結果

スレッド生成

メインスレッドとは別に、
スレッドが2つ生成されていることが確認できる

WebAssemblyのロード

workerファイルとWebAssemblyのファイルが2回呼び出されていることが確認できる

実行結果

そして文字列取得、計算結果がそれぞれ2回づつ表示されていることが確認できる

(^_^;) 動いた・・・うごいたぁ。。。

さて、次はパフォーマンスも測ってみよう。

参考:
バンドラを使わずにRustをWASMにする

------------------- ↓ 後書はここから ↓-------------------

拡張子cjsとmjs

さて、上記サンプルにてjsファイルの拡張子をcjsにしたりmjsにしたりしたのにお気づきだろうか。
これは意図してこうしている
(別に全部.jsで良いんだけど)

まずWorkerファイル(worker.cjs)はESモジュール非対応だ。
正確に言うと importScripts メソッドがESモジュール非対応。
(worker自体は下記のように記述することでESM対応できる)

new Worker("worker.mjs", {type: "module"})

そしてwasm-pack(target=no-module)から生成されたファイルは、
importScriptsを使う以外に呼び出す手段がない。
なので、この形式以外で実装は無理だろう。

実実装を考えると、
workerファイルは最小限にしたいところだが、
WebAssemblyに渡す前処理、後処理でまぁまぁ膨らむと思う。

🤔 悩ましい

ターゲットWebの場合

no-modulesだとバンドラーと相性が悪いので、
なんとかESモジュールとして使いたい。
ではターゲットをwebにした場合は果たして動くのか。

wasm-pack build --release --target web --out-dir ./dist/pkg --out-name wasm-worker

フロントエンドを調整

Workerのインスタンス生成時のオプションに {type:"module"}を追加する

./dist/js/index.mjs
(async () => {
const maxWorkers = 2
const workers = []
const path = new URL('/js/worker.mjs', import.meta.url)  

for (let i = 0; i < maxWorkers; i++) {
  workers[i] = new Worker(path, {type:"module"})
  workers[i].addEventListener('message', (event) => {
      console.log("Data from worker"+i+" received: ", event.data);
  }, false);
}
})()

importを使用して各種モジュールを読み込むように調整

./dist/js/worker.mjs
import wasm_bindgen, { greet, add, fibonacci } from "/pkg/wasm-worker.js"
(async () => {
  const path = new URL("/pkg/wasm-worker_bg.wasm", import.meta.url)  
  await wasm_bindgen(path)

  console.log(greet("dozo"))
  console.log(add(2,5),fibonacci(30))
})()

結果

(・∀・) やったか!?


  1. rust-toochain.toml でtoolchainの設定
  2. rustup toolchain list で確認
  3. .cargo/config.toml でターゲット、ビルドオプションの設定
  4. cargo +nightly config get -Z unstable-options で確認
  5. cargo check --release で確認
  6. wasm-pack build --target no-modules で出力
  7. load no-module jsの生成

no-moduleロード

wasm-pack
wasm-bindgen
rollup/plugin-wasm ×
wasm-tool/rollup-plugin-rust ×
rustup toolchain add nightly
cargo add wasm-bindgen rayon wasm-bindgen-rayon
./rust-toolchain.toml
[toolchain]
channel = "nightly"
rustup toolchain list
  stable-x86_64-unknown-linux-gnu (default)
  nightly-2021-02-11-x86_64-unknown-linux-gnu (override)
./.cargo/config.toml
[build]
target = "wasm32-unknown-unknown"

[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+atomics,+bulk-memory"]

[unstable]
build-std = ["panic_abort", "std"]
cargo +nightly config get -Z unstable-options
  build.target = "wasm32-unknown-unknown"
  target.wasm32-unknown-unknown.rustflags = ["-C", "target-feature=+atomics,+bulk-memory"]
  unstable.build-std = ["panic_abort", "std"]
./src/lib.rs
pub use wasm_bindgen_rayon::init_thread_pool;

use rayon::prelude::*;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn sum(numbers: &[i32]) -> i32 {
    numbers.par_iter().sum()
}
./src/lib.rs
#[no_mangle]
fn add(x: i32, y:i32) -> i32 {
    x + y
}

#[no_mangle]
fn fibonacci(num: i32) -> i32 {
    match num {
        0 => 0,
        1 => 1,
        _ => fibonacci(num-1) + fibonacci(num-2),
    }
}
import wasm from './pkg/car_fib_bg.wasm';

(async () => {
    const modules = await wasm({});
    console.log(modules.instance)
    console.log(modules.instance.exports.add(10,3)); // 13
    console.log(modules.instance.exports.fibonacci(20)); //6765
    

    const bytes = await fetch('pkg/car_fib_bg.wasm');
    const response = await bytes.arrayBuffer();
    const result = await WebAssembly.instantiate(response, {});
    console.log(result.instance.exports.add(10,3)); // 13
    console.log(result.instance.exports.fibonacci(20)); //6765
//    const response = await bytes.arrayBuffer();
//    const result = await WebAssembly.instantiate(response, {});
//    console.log(result.instance.exports.add(10,3)); // 13
//    console.log(result.instance.exports.fibonacci(20)); //6765
})();
import { wasm } from '@rollup/plugin-wasm'
import serve from "rollup-plugin-serve"

const watch = process.env.ROLLUP_WATCH

export default {
    input: "./load.mjs",
    output: {
        file: './load.js',
    },
    plugins: [
        wasm({
            publicPath: "./"
        }),
        watch && serve({
            contentBase: "./",
            mimeTypes: {
              "application/wasm": ["wasm"],
            },
        })        
    ]
}

コンパイル時にエラーが出る

尋常じゃなく長いエラー文

❯ wasm-pack build --target web --release --out-name svelte-wasm
[INFO]: Checking for the Wasm target...
[INFO]: Compiling to Wasm...
・・・
   Compiling svelte-wasm v0.1.0 (/home/dozo/Repos/Rust/svelte-wasm)
error: linking with `rust-lld` failed: exit status: 1
・・・
  = note: rust-lld: error: mutable global exported but 'mutable-globals' feature not present in inputs: `__tls_base`. Use --no-check-features to suppress.
          rust-lld: error: mutable global exported but 'mutable-globals' feature not present in inputs: `__tls_size`. Use --no-check-features to suppress.
          rust-lld: error: mutable global exported but 'mutable-globals' feature not present in inputs: `__tls_align`. Use --no-check-features to suppress.


error: aborting due to previous error

nightlyのバージョンが新しすぎる。

(^_^;) な、何を言ってるかわからねーと思うが。

wasm-bindgen-rayon はバージョン1.50で動作するように作られているので、
それより新しいtoolchainでコンパイルしようとすると、
上記のように弾かれる。

なのでマニュアル指定の nightly-2021-02-11 バージョンにしておく。

rustup toolchain add nightly-2021-02-11
printf '[toolchain]\nchannel = "nightly-2021-02-11"' | tee rust-toolchain.toml
rustup toolchain list
❯ cargo clean && cargo check --release
・・・
    Checking wasm-bindgen-rayon v1.0.3
error: Did you forget to enable `atomics` and `bulk-memory` features as outlined in wasm-bindgen-rayon README?
  --> /home/dozo/.cargo/registry/src/-7959054cb1a36f23/wasm-bindgen-rayon-1.0.3/src/lib.rs:15:1
   |
15 | compile_error!("Did you forget to enable `atomics` and `bulk-memory` features as outlined in wasm-bindgen-rayon README?");
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

error: could not compile `wasm-bindgen-rayon`

To learn more, run the command again with --verbose.

おまえ README 読んだ?マジで。
ってエラーメッセージ煽られる

(゚Д゚)ハァ?Zakkennna--

確かにコンパイルオプションを設定しろとREADMEには書いてあるので、
癪だが設定してやろう。癪だが。

printf '\n[target.wasm32-unknown-unknown]\nrustflags = ["-C", "target-feature=+atomics,+bulk-memory"]' | tee -a ./.cargo/config.toml
printf '\n[unstable]\nbuild-std = ["panic_abort", "std"]' | tee -a ./.cargo/config.toml

Discussion