WebAssembly: data セクションの最適化


概要

WebAssembly 逆アセンブル1 と Rust 製の WebAssembly オプティマイザの twiggy を駆使して、WebAssembly ファイル中の data セクションのサイズ削減を図る

対象読者

  • WebAssembly を極限まで小さくするための、いかなる苦労を厭わない人
    • この記事中に、ツールを使えば簡単に WebAssembly を極限まで小さくできる、魔法のような方法が紹介されていると期待していた人はブラウザバック推奨
  • WebAssembly Text Format にアレルギーのない人

使用ツール

WebAssembly for VSCode

Visual Studio Code 上で、次の機能を提供する拡張機能

  • WebAssembly を WebAssembly Text Format に変換する
  • WebAssembly Text Format の色付け

インストール方法

Visual Studio Code の拡張機能ペインから、WebAssembly と検索することでインストール可能

twiggy

Rust 製の WebAssembly 最適化のユーティリティツール。次に紹介するようなコマンドを備えている

  • twiggy top
    • どの部分がどのくらいのサイズを食っているかを一覧表示する
  • twiggy garbage
    • 不要と思われる部分を一覧表示する

インストール方法

Rust をインストールした上で、次のコマンドを実行する

cargo install twiggy

WebAssembly のデータセクションの前提知識

他プラットフォームの実行形式では、静的格納領域に .bss セクション(初期化が不要な領域)と .data セクション(初期化が必要な領域)の区別がある
この .bss セクションに格納されるべきものが増えても、実行形式のファイルサイズには影響を与えない

しかし、WebAssembly には .bss セクションに該当するセクションは存在しない
そのため、.bss セクションに格納されるべきものも、データセクションに格納されてしまい、WebAssembly のファイルサイズを大きくしてしまう (特にこれはデバッグビルドで顕著である)

ただ、WebAssembly の起動時に WebAssembly の線形メモリがゼロ初期化されていることが期待できる (むしろゼロ初期化されてなければ大問題である)
それを利用して、次のように複数のデータセクションに分けることで、データセクションのトータルサイズが小さくすることができる (これは、リリースビルドで自動的に有効になる)

  (data $d0 (i32.const 1026) "\80?")
  (data $d1 (i32.const 1046) "\80?")
  (data $d2 (i32.const 1066) "\80?")

不要そうなデータセクションの ID を検索する

次のコマンドを実行する

twiggy garbage <対象の.wasm ファイル> --show-data-segments > DataDump.txt

すると、次のような出力が得られる

DataDump.txt
 Bytes   │ Size % │ Garbage Item
─────────┼────────┼───────────────────────────────
 2357457 ┊ 15.57% ┊ custom section '.debug_info'
 1519400 ┊ 10.03% ┊ custom section '.debug_str'
 1116168 ┊  7.37% ┊ custom section '.debug_loc'
 1098687 ┊  7.25% ┊ custom section '.debug_line'
  160386 ┊  1.06% ┊ custom section '.debug_abbrev'
  119256 ┊  0.79% ┊ custom section '.debug_ranges'
   56690 ┊  0.37% ┊ data[3430]
   44189 ┊  0.29% ┊ data[1052]
   31550 ┊  0.21% ┊ data[312]
   29917 ┊  0.20% ┊ data[1181]
  911216 ┊  6.02% ┊ ... and 3564 more
 7444916 ┊ 49.16% ┊ Σ [3574 Total Rows]

ここからは、data[3430] が不要なデータかどうかを検証していく

不要そうなデータセクションのコードベースを探す

Visual Studio Code のエクスプローラペインで、対象の WebAssembly ファイルを右クリックし、Show WebAssembly をクリックする

すると、次のように WebAssembly Text Format が表示される

この WebAssembly Text Format のうち、data[3430] を探しに行く
WebAssembly Text Format では、データセクション ID は $d<セクションID> という形式をとっているので、d3430 と検索すると便利である。

(data $d3428 (i32.const 1290470) "\f0\bf\00\00\
(data $d3429 (i32.const 1290926) "\e0?\00\00\00
(data $d3430 (i32.const 1291006) "\e0?\00\00\00
(data $d3431 (i32.const 1347696) ">\b4\e43\09\9
(data $d3432 (i32.const 1348764) "N\0e\00\00O\0

すると、静的領域のアドレス値が割り出せる (ここでは 1291006) ので、今度はこのアドレス値で検索していく
ドンピシャなアドレス値が検索にかかることは稀なので、12910 とすこし曖昧に検索することとする

今回は次の部分が検索に引っかかったので、このコード断片が含まれる関数名まで遡ると、_vp_couple_quantize_normalize であった。

   i32.const 2
   i32.shl
   i32.const 1291072
   i32.add
   f32.load
   f32.store

emscripten のライブラリキャッシュ中で _vp_couple_quantize_normalize と検索すると、これは libvorbis.a の psy.c に含まれる関数であることが判明した

関数中に FLOOR1_fromdB_LOOKUP という、サイズが 1KB のグローバル変数があり、これがデータセクションに載っていることがわかった

ただ、今回の調査では libvorbis.a を使う限り、これは削減不可能なものであることが判明しただけであった

あとがき

今回のケースでは、データセクションの削減に至らなかった

しかし、同様の調査方法で libjpeg-turbo.a のデータセクションの削減に挑んだとき、jpeg_nbits_table.h の内容を、WebAssembly のオプコード (i32|i64.clz) を使うことで、削減可能と判明し、結果 100KB 程度の削減に成功できたケースも存在する

地味で労力のかかる方法ではあるが、現状これに勝る方法がない限り、この方法に頼らざるを得ない状況は変わらないであろう (もっとも、データアドレスとコードベースのマッピングは DWARF に含まれているはずなので、それを使いこなせるようになれば少しは楽になるであろうか)

また、C++ の型情報もデータセクションサイズに大きいインパクトを与えうる。今一度、WebAssembly をビルドするときに、不要なコードを含めてしまっていないかを十分に検討してほしい (そのために、コードカバレッジツールを emscripten で使えるようにするための方法を模索している)


  1. .wasm を .wat に変換すること