WASM に入門してみた
はじめに
こんにちは。株式会社バニッシュ・スタンダードの muship です。
近年注目されてる WASM ですが中々情報が少なくとっつきにくかったので敬遠してたんですが、
AI の発達や他エンジニアさんのおかげもあって情報が増えてきたこともあり今なら勉強しやすいのでは?と思ったので入門してみました。
WASM とは?
WebAssembly(WASM)は、高水準言語(C++、Rust、Go など)を低水準言語のバイナリ形式(.wasm)に変換し、ブラウザ内の仮想環境で実行できるようにする技術のことです。
高度な計算や負荷の高い処理をJavaScriptより高速に完結できるメリットがあります。
向いているアプリケーション
-
画像/動画処理
- フィルター適用、リアルタイム編集(本記事で扱う例)
-
ゲーム開発
- 3Dレンダリング、物理演算、ゲームエンジンのポーティング
-
暗号化/復号処理
- セキュアな計算を高速に行う必要がある場合
-
機械学習
- モデルの推論処理をクライアントサイドで実行
-
CAD/3Dモデリング
- 複雑な計算を必要とする図形処理
-
データ分析
- 大量データの高速処理、可視化
実行されるまでの流れ
- 元となるコード(C++、Rust、Go など)から .wasm バイナリファイルを生成する
- ブラウザが起動時に .wasm をダウンロードし、WASM エンジンが読み込む
- ブラウザが実行時に .wasm バイナリを現在のデバイス(PC)の機械語に変換(JIT コンパイル)
- 変換されたコードがブラウザの仮想マシン内で実行され、結果がJavaScript実行環境に返される
言語の抽象度を理解する: 高水準言語・低水準言語・機械語とWASM
WASM とは?で触れた
高水準言語(C++、Rust、Go など)を低水準言語のバイナリ形式に変更し...
ですが、機械語の抽象度について理解できてなかったのでまとめてみました。
-
高水準言語
- Python / JS / Go / Rust…
- 人間に読みやすいよう高い抽象化がされている
- GC などの自動制御によりコンピューターリソース周りを意識せずかけるがパフォーマンス面で問題になるケースが多かったり、手動メモリ管理または所有権モデルなどの明示的な制御が必要だが書き手がコンピューターのリソース管理を行うことでパフォーマンス面での向上が図れたりと言語ごとに特徴がある
-
低水準言語
- アセンブリ / C …
- プロセッサの命令セットに近い / 人間が読むのが難しい
-
機械語
- CPU が直接実行する命令
- バイナリ形式であり、人間が直接扱うことはほぼない
- 各CPUアーキテクチャ固有の形式を持つ
WASMは、「共通のコンパイル先(ターゲット)」として機能します。
高水準言で書かれたコードをブラウザで直接実行できるようにする橋渡し役となります。JavaScriptがインタープリタ型で実行時に解釈されるのに対し、WASMはコンパイル済みのバイナリを実行するため処理が高速です。
試してみた
強みとしてある画像 / 動画のフィルター適用が簡単に作成できそうだったので、
「動画をリアルタイムでセピアに変換する」 アプリケーションを作ってみて WASM の流れを体感してみました。
WASM といえば Rust が一番の選択肢に上がると思いますが、Rust はまだ駆け出せてないので慣れている Go を使用してます。
出来上がったwebアプリケーションはこちらです。
.wasm の作成と読み込み
今回は React から Go で作成した .wasm を読み込むため、以下のディレクトリ構成にしました。
Go の処理は /wasm に記述し、コンパイルされた .wasm ファイルは /public に保存されるようにしています。
❯ tree -L 1
...
├── index.html
├── node_modules
├── package.json
├── public
├── src
...
└── wasm
/wasm/main.go をコンパイルするコマンドは以下になります。
GOOS=js GOARCH=wasm go build -o ../public/go.wasm main.go
-
GOOS=js
- GOOSはGo Operating Systemの略で、コンパイル対象のオペレーティングシステムを指定します
- 今回はJavaScriptで実行したいので js を指定してます
-
GOARCH=wasm
- GOARCHはGo Architectureの略で、コンパイル対象のアーキテクチャを指定します
- WebAssemblyの場合は、
wasm
を指定
ビルドされたWasmを実行するには,Goが提供しているJSのグルーコードを利用します。
cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" ../public/
React 側で .wasm ファイルを読み込んでないとエラーになるため、index.html で wasm_exec.js ファイルを読み込ませます。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="./wasm_exec.js"></script>
<title>WASM テスト</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
React 側で WASM を初期化・実行するコードは以下のようになります。
const go = new window.Go();
const result = await WebAssembly.instantiateStreaming(
fetch("/go.wasm"),
go.importObject,
);
go.run(result.instance);
所感
「そもそもアセンブリとか通さず最初からバイナリにしちゃえばよくない?」って思ったんですが、そうするとCPUアーキテクチャ(x86とかARMとか)ごとにバイナリを作る必要があり、CPU アーキテクチャに依存してしまいます。
公式でも記載があり「Platform-independent」でプラットフォームに依存しないことが設計思想としてあるのでアセンブリで提供している理解です。
おわりに
今回は WASM ってどういうものかを理解するために概要と呼び出し周りを中心にまとめてみました。
ただ今回作ってみたアプリケーションだと WASM の恩恵はあまり受けられてないと思うので、次回はより高負荷な機能(例えば顔認識や複雑なフィルタ処理)を実装してJavaScript実装との比較ベンチマークとかできたらいいなと思ってます。
DOM操作やブラウザAPIの実行などもできるみたいなのでいろいろ試してみようと思います。
Discussion