Web Worker+WebAssemblyでマルチスレッド: Rustが征く(10)
Wasm+WebWorkerは使い方が限定的
関連記事:
Rustが征くシリーズ過去記事
- WSL2で作るWindows開発環境: Rustが征く(1)
- wasmerでWebAssemblyの門を叩く: Rustが征く(2)
- JavaScriptからWebAssemblyの関数を呼び出す: Rustが征く(3)
- TypeScriptとRollupでWebassemblyを稼働させる: Rustが征く(4)
- SvelteでWebAssemblyでTypeScriptでRollupで: Rustが征く(5)
- WASMのサイズでかすぎね?: Rustが征く(6)
- Webassemblyマルチスレッド用crateを調べる: Rustが征く(7)
- WebAssemblyでSleep: Rustが征く(8)
- Web Workerでマルチスレッド(JSのみ): Rustが征く(9)
- Web Worker+WebAssemblyでマルチスレッド: Rustが征く(10) ← イマココ
------------------- ↓ 前書はここから ↓-------------------
前回の記事で、
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
フロントエンドを設置
各種フロントエンドを準備
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<script type="module" src="./js/index.mjs"></script>
</body>
</html>
ワーカーを2つ生成
(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に読み込ませる独立したファイルを設置
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表示の関数が書いてあるが、
全部消して以下に書き換える。
文字列やりとりと足し算とフィボナッチ数列計算をさせてみる
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で頑張る系のサーバーファイルを用意
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回づつ表示されていることが確認できる
(^_^;) 動いた・・・うごいたぁ。。。
さて、次はパフォーマンスも測ってみよう。
------------------- ↓ 後書はここから ↓-------------------
拡張子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"}
を追加する
(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を使用して各種モジュールを読み込むように調整
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))
})()
結果
(・∀・) やったか!?
- rust-toochain.toml でtoolchainの設定
- rustup toolchain list で確認
- .cargo/config.toml でターゲット、ビルドオプションの設定
- cargo +nightly config get -Z unstable-options で確認
- cargo check --release で確認
- wasm-pack build --target no-modules で出力
- 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
[toolchain]
channel = "nightly"
rustup toolchain list
stable-x86_64-unknown-linux-gnu (default)
nightly-2021-02-11-x86_64-unknown-linux-gnu (override)
[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"]
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()
}
#[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