RustでWASMを作成したメモ
環境:Windows
wasm-packを入れる
cargo install wasm-pack
するが、以下のようなエラーが出る。
error: failed to run custom build command for `openssl-sys v0.9.77`
追記:
cargo install wasm-pack --no-default-features
こっちを実行すると良い。
Rustのバージョンが古かったのかなと思い、
rustup update
してみるものの、エラー。
VSCode特有の問題らしい。
openssl-sysのビルドがこける問題は結構みんなはまってるらしい。
OpenSSLをインストール、そして上の記事に従い環境変数を設定。
しかしうまくいかない…
と思ったが、環境変数を再読み込みしてないだけだった。
と思ってターミナルを再起動したがだめ。
調子に乗ってOpenSSL v3.0.7を入れたが、
OpenSSLをインストールする上での注意点としては以下2点です。
サポートされているのは OpenSSL 1.0.1 ~ 1.1.1 と LibreSSL 2.5 ~ 3.4.1
環境変数OPENSSL_DIR(+OPENSSL_LIB_DIR+OPENSSL_INCLUDE_DIR)を設定する
バージョンが新しすぎても駄目っぽい。
OpenSSL 1.1.1入れてもうまくいかず…
cargo install wasm-pack --no-default-features
このオプション付きでやるといいらしい。
サンプルを用意
これに沿ってやる。
雛形を作って
cargo new --lib hello-wasm
サンプルコードに書き換え
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
ファイルを保存すると
extern {
が
extern "C" {
に勝手に書き換えられる。
なんでだろ。ここでいうCはC言語?Javascriptに書き換えなくて大丈夫?
とりあえずCargo.tomlに以下を追記
...
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
wasm-packでビルドする
wasm-pack build --target web
pkgフォルダが出来た。
HTMLを用意する
プロジェクトルートに用意する。
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<title>hello-wasm example</title>
</head>
<body>
<script type="module">
import init, { greet } from "./pkg/hello_wasm.js";
init()
.then(() => {
greet("WebAssembly")
});
</script>
</body>
</html>
ローカルサーバーで動作確認
npmが使える環境で以下を実行。
npx http-server
いけた。
生成されたJavascriptを覗いてみる
前評判ではwasmでやり取りできるのは数字だけとか可変長配列は難しいとか何か色々見た気がするので、どうやって可変長文字列を渡しているのか気になる。
export function greet(name) {
const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
wasm.greet(ptr0, len0);
}
Javascriptから見える引数はname
ひとつだけだが、WASMを呼び出すときはポインタと長さを渡してる。
再ビルドしようとしたらできなくなった
一回目はできたのに、同じコマンドを二度目実行すると駄目になった。
$ wasm-pack build --target web
[INFO]: Checking for the Wasm target...
[INFO]: Compiling to Wasm...
Finished release [optimized] target(s) in 0.03s
[INFO]: Installing wasm-bindgen...
Error: failed to download from https://github.com/WebAssembly/binaryen/releases/download/version_90/binaryen-version_90-x86-windows.tar.gz
To disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
これをCargo.tomlに書き加えると治る。これやるのは負けな気がする。
これGithubにbotと思われて弾かれてるのかな。
解決は難しそう。
手動インストールするしかないのかなぁ。
ボードゲームの思考ロジックのWASM化に挑戦してみる
これのwasm化
Rust移植はもうできてる。あとはWASM化するだけ。
JS側に公開するメソッドを用意。
なんかタプル使えなかったり色々面倒だったので引数も戻り値もStructにした。
MapArrayの正体はVec<isize>で、そのまま引数にしようとするとエラーになる。構造体として公開するとOK。うーん。
use wasm_bindgen::prelude::wasm_bindgen;
use crate::rule::MapArray;
pub mod ai;
pub mod eval;
pub mod rule;
#[allow(dead_code)]
#[wasm_bindgen]
pub struct Result {
from: usize,
to: usize,
}
#[wasm_bindgen]
pub struct Arg {
map: MapArray,
turn_player: isize,
depth: isize,
}
#[wasm_bindgen]
pub fn think_ai(arg: Arg) -> Result {
let result = ai::think_ai(&arg.map, arg.turn_player, arg.depth, None, None, None);
Result {
from: result.0.unwrap().0,
to: result.0.unwrap().1,
}
}
ビルド
getrandomでエラーになる。
getrandomというメソッドを自分は用意した覚えはないが、randクレートは利用している。
wasm-pack build --target web
error: the wasm32-unknown-unknown target is not supported by default, you may need to enable the "js" feature. For more information see: https://docs.rs/getrandom/#webassembly-support
--> C:\Users\gabill\.cargo\registry\src\github.com-1ecc6299db9ec823\getrandom-0.2.8\src\lib.rs:263:9
|
263 | / compile_error!("the wasm32-unknown-unknown target is not supported by \
264 | | default, you may need to enable the \"js\" feature. \
265 | | For more information see: \
266 | | https://docs.rs/getrandom/#webassembly-support");
| |________________________________________________________________________^
error[E0433]: failed to resolve: use of undeclared crate or module `imp`
--> C:\Users\gabill\.cargo\registry\src\github.com-1ecc6299db9ec823\getrandom-0.2.8\src\lib.rs:290:5
|
290 | imp::getrandom_inner(dest)
| ^^^ use of undeclared crate or module `imp`
For more information about this error, try `rustc --explain E0433`.
error: could not compile `getrandom` due to 2 previous errors
今回高速化したかった範囲ではないので、randを利用してる箇所を泣く泣く削除する。
調べれば回避方法はあるかもしれない。
疑似乱数を自前で実装してしまうのも良い。その方がテストやりやすくなるし。
メモ:randを使いたい場合はこの記事を読む
[INFO]: Installing wasm-bindgen...
Error: failed to download from https://github.com/WebAssembly/binaryen/releases/download/version_90/binaryen-version_90-x86-windows.tar.gz
To disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.
またこれでビルドできない。
最適化はいったん諦めて、
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
でやる。
手動インストールすれば何とかなるかもしれないが、それよりも誰でも気軽にビルドできる方を今は取りたい。
Javascriptを確認してみる
/**
* @param {Arg} arg
* @returns {Result}
*/
export function think_ai(arg) {
_assertClass(arg, Arg);
var ptr0 = arg.ptr;
arg.ptr = 0;
const ret = wasm.think_ai(ptr0);
return Result.__wrap(ret);
}
/**
*/
export class Arg {
__destroy_into_raw() {
const ptr = this.ptr;
this.ptr = 0;
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_arg_free(ptr);
}
}
/**
*/
export class Result {
static __wrap(ptr) {
const obj = Object.create(Result.prototype);
obj.ptr = ptr;
return obj;
}
__destroy_into_raw() {
const ptr = this.ptr;
this.ptr = 0;
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_result_free(ptr);
}
}
うーん、呼び出せる気がしない。
引数用のクラスからもプロパティが消失している。
どうやって引数を渡せば良いんだ。
export class Arg {
__destroy_into_raw() {
const ptr = this.ptr;
this.ptr = 0;
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_arg_free(ptr);
}
}
あ、プロパティにpubを付け忘れてた。
rustは標準で非公開だった。
配列の渡し方が分からない
#[wasm_bindgen]
pub struct Arg {
pub map: MapArray, // ←
pub turn_player: isize,
pub depth: isize,
}
矢印のところで以下のエラーがでる。MapArrayの正体はVec<isize>。
the trait bound `Vec<isize>: std::marker::Copy` is not satisfied
the trait `std::marker::Copy` is not implemented for `Vec<isize>`
これを参考にしてみる
上のリンクの解説通り
JsValue::from_serde(&example).unwrap()
とやったら「この方法は非推奨だからもう使うな」と怒られたので、serde-wasm-bindgen
を使う。
Rustの戻り値と引数の定義はこんな感じ。
#[wasm_bindgen]
pub struct Result {
pub from: usize,
pub to: usize,
}
#[derive(Serialize, Deserialize)]
pub struct Arg {
pub map: MapArray,
pub turn_player: isize,
pub depth: isize,
}
pub fn think_ai(arg: JsValue) -> Result {
で、Javascriptにはこのように公開される。
export function think_ai(arg: any): Result;
export class Result {
free(): void;
/**
*/
from: number;
/**
*/
to: number;
}
any型…嫌な感じだが、やむを得ない。
ドキュメントをよく読んでみたら、Vecは使えないけど数字の配列のスライスは使えた。
固定長配列を試して駄目だったから諦めてたけど、スライス使えるのなら実質Vecじゃん。
という訳でRust側のコードは以下のようにする。
#[wasm_bindgen]
pub struct Result {
pub from: usize,
pub to: usize,
}
#[wasm_bindgen]
pub fn think_ai(map: &[isize], turn_player: isize, depth: isize) -> Result {
するとJavascriptのコードからもAnyが消える。
/* tslint:disable */
/* eslint-disable */
/**
* @param {Int32Array} map
* @param {number} turn_player
* @param {number} depth
* @returns {Result}
*/
export function think_ai(map: Int32Array, turn_player: number, depth: number): Result;
/**
*/
export class Result {
free(): void;
/**
*/
from: number;
/**
*/
to: number;
}
動作確認
どうやってソース管理するかはこの際置いといて、生成物を強引にViteのプロジェクトに突っ込んで動作確認だけしておきたい。
動いた!
ちゃんとwasmで動いてる。すごい!
やっぱりwasm-opt = falseは嫌だ
Docker使ってwasm-opt = falseを外すことにした。
これでビルドしたら生成されるwasmが31KB→21KBになった。
FROM rust:slim-bullseye
WORKDIR /usr/src/myapp
RUN apt update && apt install -y build-essential
RUN cargo install wasm-pack
version: '3'
services:
rust:
image: rust
volumes:
- .:/usr/src/myapp
build: ./
command: wasm-pack build --target web
ボードゲームに組み込んだ結果
Javascriptのほうがわずかに速い…なんで?
複数回実行しても結果はあまり変わらず。
レベルを下げたら実行時間は0msになるので、オーバーヘッドは無視できそう。
えぇー…。
条件
ブラウザ:Chrome
レベル:6
配置パラメータ:init=1,2,3,4,5,6,7,8
1手目:2を右上
2手目:2を上
結果
WASM
- 一手目:796ms
- 一手目:1575ms
Javascript
- 一手目:734ms
- 一手目:1457ms
FirefoxではWASMの方が速くなっていた
WASMの結果はChromeとほぼ同じ。
Javascriptの結果はChromeより倍くらい遅い。
これまでいろんなブラウザでこのボードゲームの処理速度を測ってきたけど、ブラウザによって速度はだいぶバラツキがあった。
でも今回WASMの実行速度が異なるブラウザでほぼ同じ結果になったのは興味深い。
WASM
- 一手目:798ms
- 一手目:1583ms
Javascript
- 一手目:1322ms
- 一手目:2576ms
なんでだろう
呼び出しオーバーヘッドがあったのでは?
JavascriptからWASMを呼び出す回数は一手あたり1回。WASMからJavascriptを呼び出すことはない。
レベルを1に下げた時の実行時間が0ms(ミリ秒以下四捨五入)なことから、オーバーヘッドはほぼ無視して良いレベル。
WASMの呼び出しに時間がかかってるのでは?
読み込み時間はベンチに入れてないし、仮に入れていたとしてもJavascriptをバンドルする時にWASMも全部HTMLにインライン埋込してるので読み込み時間もほぼ変わらない。
###JavascriptとRustでアルゴリズムを変えてるのでは?
どちらもこれからずっとメンテしていく予定なのでほぼ変えてない。JavascriptとRustのコードはほぼ1行:1行で対応している。
ただJavascript版はInt8Arrayを使っているが、Rust版はisizeを使っているという違いがある。
ChromeのJavascriptエンジンが優秀過ぎるのでは?
たぶんこれじゃないかと思ってる。
ちなみにこのボードゲームの思考ロジックでは、number型の配列は遅いのでInt8Arrayを使ったり、array.length
でfor文を回すと遅いからfor文を固定回数で回したり、Math.floor(n)
が遅いから~~n
で端数の処理をしたり、呼び出し回数が多いメソッドではfor文で12回ループをするのすらネックになるので処理を12回ベタ書きまでしている。可読性を酷く犠牲にしてまで速く動くように書いているので、もともと最適化が効いていた可能性は高い。
コンパイルのオプションがちゃんとチューニングされていないのでは?
これもある。
いちおう最適化レベルは3になっているので、変な設定にはなっていないはず。
でも自分はRust歴がまだ1ヶ月ちょっとと浅いので、みんなが当たり前にやっているチューニング設定をやっていない可能性は高い。
この動画の01:05:16から始まる講演、なるほどなぁ。
#[inline]をつけてみる
#[inline]を付けてinline化してみた。
まだJavascriptには叶わないが、わずかに改善。
複数回測っても傾向は変わらなかった。
バイナリサイズは21KB→20KBに減った。
WASM(inlineなし)
一手目:796ms
二手目:1613ms
WASM(inlineあり)
一手目:773ms
二手目:1501ms
Javascript
一手目:710ms
二手目:1413ms
固定長配列にしてみる
Rust版ではJavascript版のアルゴリズムを忠実に再現したと上の方に書いたが、ちょっと嘘がある。
JavascriptではInt8Arrayを使っているが、RustではVec<isize>を使ってる。Vecは便利だが贅沢過ぎる。まずは固定長配列[isize;56]にしてみる。
WASM(Vec(isize)→[isize;56])
一手目:698ms
二手目:1389ms
ついにRust版がJavascript版をわずかに上回った。
isizeからi8にしてみる
WASM(isize → i8)
一手目:547ms
二手目:1139ms
だいぶ速度が増した。
WASMってi32、i64、f32、f64しかない認識なんだけど、なんで速くなるんだろう。
よく分からないけどWASMにはi32.store8という8bit整数をコンパクトに格納する命令もあるらしい。
i8を使うのもまったく無駄というわけではなさそう。