Open8

wasm-pack の最小出力を探す

mizchimizchi

wasm の現在の課題はビルドサイズであり、可能な限り小さいビルドサイズにしたい。それをオプションをガチャガチャやりながら探す

mizchimizchi

M1 Mac での準備

M1 mac の場合、wasm-pack 標準のインストールだと wasm-opt の prebuilt なバイナリが見つけられずビルド後の最適化ができずに.wasm の出力が大きくなる。

https://github.com/rustwasm/wasm-pack/issues/913

こちらの fork をインストールするとうまくいったが…ちょっと古いバージョンになってしまうので、解消してほしい

cargo install wasm-pack --git https://github.com/d3lm/wasm-pack --rev 713868b204f151acd1989c3f29ff9d3bc944c306

あと binaryen もいれておく

brew install bynaryen
mizchimizchi

まず、普通にビルドしてみる。

$ wasm-pack build
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
   Compiling proc-macro2 v1.0.36
   Compiling unicode-xid v0.2.2
   Compiling syn v1.0.86
   Compiling log v0.4.14
   Compiling wasm-bindgen-shared v0.2.79
   Compiling cfg-if v1.0.0
   Compiling lazy_static v1.4.0
   Compiling bumpalo v3.9.1
   Compiling wasm-bindgen v0.2.79
   Compiling quote v1.0.15
   Compiling wasm-bindgen-backend v0.2.79
   Compiling wasm-bindgen-macro-support v0.2.79
   Compiling wasm-bindgen-macro v0.2.79
   Compiling console_error_panic_hook v0.1.7
   Compiling mywasm v0.1.0 (/Users/kotaro.chikuba/mizchi/rust-tokenizer/mywasm)
warning: function is never used: `set_panic_hook`
 --> src/utils.rs:1:8
  |
1 | pub fn set_panic_hook() {
  |        ^^^^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: `mywasm` (lib) generated 1 warning
    Finished release [optimized] target(s) in 5.51s
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: found wasm-opt at "/Users/kotaro.chikuba/brew/bin/wasm-opt"
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨   Done in 5.69s
[INFO]: 📦   Your wasm pkg is ready to publish at /Users/kotaro.chikuba/mizchi/rust-tokenizer/mywasm/pkg.
[WARN]: ⚠️   There's a newer version of wasm-pack available, the new version is: 0.10.2, you are using: 0.10.0. To update, navigate to: https://rustwasm.github.io/wasm-pack/installer/

出力を見る

$ ls -lh pkg/*.wasm | awk '{print $9,$5}'
pkg/mywasm_bg.wasm 253B

binaryen で入れた wasm2wat で中身をみてみる

wasm2wat pkg/mywasm_bg.wasm
(module
  (type (;0;) (func (param i32 i32)))
  (type (;1;) (func))
  (import "./mywasm_bg.js" "__wbg_alert_8093a2ef8ddf8b97" (func (;0;) (type 0)))
  (func (;1;) (type 1)
    i32.const 1048576
    i32.const 14
    call 0)
  (memory (;0;) 17)
  (export "memory" (memory 0))
  (export "greet" (func 1))
  (data (;0;) (i32.const 1048576) "Hello, mywasm!\00\00\04"))
mizchimizchi

ブラウザで実行する環境を作っておく

yarn init -y
yarn add -D vite typescript
main.tsx
import init, {greet} from "./mywasm/pkg/mywasm.js";
await init();

greet();
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h1>hello</h1>
  <script type="module" src="./main.tsx"></script>
</body>
</html>

alert だと同期ブロックして面倒なので、conosle.log に変えておく

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

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub fn init_debug() {
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}

#[wasm_bindgen]
pub fn greet() {
    log("Hello, mywasm!");
}

target に web を指定して実行する

(cd mywasm && wasm-pack build --target web --release)
yarn vite

console.log で Hello, mywasm! と出ていれば成功

mizchimizchi

ビルドサイズのメモ

この状態でオプションの有無のビルドサイズを見てみる

  • features: [console_error_panic_hook] : 30k
  • features: [] : 1.3k
  • `features: [wee_alloc] 258b

プロダクションは wee_alloc 有効、 panic_hook 無効にしたほうが良さそう


もっと現実的なコードにしてみる。今回は https://github.com/mizchi/mints/tree/main/packages/mints の parser generator を書き直したくて、 https://zenn.dev/nojima/articles/05cb9ffa0f993b の記事を読みながら、一番最初の digits のパースだけのコードを入れてみる。

use wasm_bindgen::prelude::*;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub fn init_debug() {
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}

// https://zenn.dev/nojima/articles/05cb9ffa0f993b
#[wasm_bindgen]
pub fn parse(s: &str) -> i64 {
    let (parsed, _rest) = digits(s).unwrap();
    parsed
}

// s の先頭にある整数をパースし、整数値と残りの文字列を返す。
pub fn digits(s: &str) -> Option<(i64, &str)> {
    let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
    match s[..end].parse() {
        Ok(value) => Some((value, &s[end..])),
        Err(_) => None,
    }
}

これで std の Option や string 周りのコード等がこれでビルドされるようになり、ビルドサイズが増える。

$ wasm-pack build --target web -- --features wee_alloc && ls -lh pkg/*.wasm
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
    Finished release [optimized] target(s) in 0.01s
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: found wasm-opt at "/Users/kotaro.chikuba/brew/bin/wasm-opt"
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨   Done in 0.22s
[INFO]: 📦   Your wasm pkg is ready to publish at /Users/kotaro.chikuba/mizchi/rust-tokenizer/mywasm/pkg.
[WARN]: ⚠️   There's a newer version of wasm-pack available, the new version is: 0.10.2, you are using: 0.10.0. To update, navigate to: https://rustwasm.github.io/wasm-pack/installer/
-rw-r--r--  1 kotaro.chikuba  staff    15K  1 27 15:34 pkg/mywasm_bg.wasm

一気に 15k。 やりがいがでてきた。

mizchimizchi

ブラウザ側から呼べることを確認する

import init, {parse, init_debug} from "./mywasm/pkg/mywasm.js";

init().then(() => {
  init_debug();
  console.log(parse("123yen"));
});

123n と出る。i64 だと返り値が JS 側で受け取ると BigNum になる。

rust をi32 に修正

use wasm_bindgen::prelude::*;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub fn init_debug() {
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}

// https://zenn.dev/nojima/articles/05cb9ffa0f993b
#[wasm_bindgen]
pub fn parse(s: &str) -> i32 {
    let (parsed, _rest) = digits(s).unwrap();
    parsed
}

// s の先頭にある整数をパースし、整数値と残りの文字列を返す。
// パースに失敗した場合は None を返す。
pub fn digits(s: &str) -> Option<(i32, &str)> {
    let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
    match s[..end].parse() {
        Ok(value) => Some((value, &s[end..])),
        Err(_) => None,
    }
}

mizchimizchi

https://zenn.dev/dozo/articles/14b76b561f3b45 を読んで、最適化オプションをいじってみる

cargo.toml
[profile.release]
lto = true
codegen-units = 1
opt-level = "z"
$ wasm-pack build --target web --release -- --features wee_alloc && ls -lh pkg/*.wasm
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
   Compiling mywasm v0.1.0 (/Users/kotaro.chikuba/mizchi/rust-tokenizer/mywasm)
    Finished release [optimized] target(s) in 0.15s
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: found wasm-opt at "/Users/kotaro.chikuba/brew/bin/wasm-opt"
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨   Done in 0.40s
[INFO]: 📦   Your wasm pkg is ready to publish at /Users/kotaro.chikuba/mizchi/rust-tokenizer/mywasm/pkg.
[WARN]: ⚠️   There's a newer version of wasm-pack available, the new version is: 0.10.2, you are using: 0.10.0. To update, navigate to: https://rustwasm.github.io/wasm-pack/installer/
-rw-r--r--  1 kotaro.chikuba  staff    16K  1 27 15:45 pkg/mywasm_bg.wasm

15k => 16k 増えてしまった。

mizchimizchi

一旦 wasm-pack をやめて、 nostd でいってみる。

http://cliffle.com/blog/bare-metal-wasm/

nostd を試す

// src/lib.rs

#![no_std]
#![no_main]

#[panic_handler]
fn handle_panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
pub extern fn the_answer() -> u32 {
    42
}
$ rustc -C opt-level=s --target wasm32-unknown-unknown ./src/lib.rs && wasm-opt -Oz -o out.wasm lib.wasm
...

$ ls -lh *.wasm | awk '{print $9,$5}'
lib.wasm 153B
out.wasm 103B

これが理論上最小っぽいが、やはり色々とコードを追加すると増えていく

雑に deno で読んでみた

$ deno
Deno 1.18.0
exit using ctrl+d or close()
> const m = await WebAssembly.instantiate(await Deno.readFile('lib.wasm'));
undefined
> m.instance.exports.the_answer()
42