WebAssemblyをちょろっと触って速度測ってみる。
はじめに
この記事は、私が所属している田胡研究室のアドベントカレンダー2日目の記事です。 adventar.org
記事概略
徐々に話題になっている感のある”WebAssembly”の
- どのようなものか
- なにができ、何ができないか
- 実際の使い方
- 速度
について書きます。
具体的にはAseemblyScriptをコンパイルしWebAssemblyのバイナリを作り出し、円周率計算を高速化。 素のJavaScriptと処理時間比較を行います。
私がWebAssemblyを触るのは今回が初です。内容は駆け出し程度です、ちょっと興味ある方が触ってみる際の手助けになれば幸いです。 間違っていたら優しくご指摘いただければと。。。。。。
実際に書いてやってみるところをメインにしたいので、WebAssembly自体の解説はちょろっとだけ書きます。 たくさんのわかりやすい&詳しい説明があるのでググってください。
WebAssemblyとは
徐々に検索されるようになってる気がする。
すごく単純にいうと、JavaScriptを早く動かそうという技術。
近年では、フロントエンド(JavaScript)において高度な描画処理(WebGL等)を行ったり、ブラウザでマイニングしたりと、JavaScriptにも高速処理が求められるようになってきました。 しかし、JavaScriptはインタプリタ言語、動的型付けがあるため、高速に動かすことは難しいです。
これを解決するためにasm.jsというJavaScriptのサブセットが作られました。 asm.jsにより型は確定可能になり、ブラウザが実行前にコンパイルをできるようになりました。 これにより数値計算の高速化に成功しました。
また、C言語等からのasm.jsへのコンパイルが可能になり、ゲームエンジン等をブラウザで動かすことができるようになりました。
イケイケなasm.jsなのですが欠点があります。 まず第一に、asm.jsのコードサイズが大きいということです。 ブラウザで実行するということはネットワークで配信するということです。 webpackのビルド後の容量をひたすら削ったりとフロントエンドエンジニアが頑張っている中コードサイズが大きいというのは大きな欠点です。 また、asm.jsはJavaScriptで表現されています。 よって構文解析に時間を要します。
このような点からWebAssemblyが出てきました。WebAssemblyはasm.jsに対し
- JSではなくバイナリフォーマットを利用 => 容量の削減&構文解析の高速化
- 多様なCPU命令に対応
といった点で有利です。
WebAssemblyは、現在、主にUnity等のゲームを、ブラウザ上で動かすために使われていることが多い気がします。
asm.jsとWebAssemblyの違いについて、より詳しくは
こちらなどをご覧ください。
WebAssemblyでできること、できないこと
ざっくりWebAssemblyが何をするものなのか書きましたが、できることできないことがあります。 私自身、触る前誤解していたところもちょっとあったので、書こうと思います。
できること
基本的にはこれです。
できないこと
- DOMとかはWebAssemblyではできません(今の所)
- WebAPIとかは対応を進めている最中です
つまり?
WebAssemblyは現時点でフロントエンド計算のアクセラレータのようなものです。 JavaScriptのメインを置き換えるものではありません。
このあと実際に使えばわかりますが、WebAssemblyのバイナリファイルへのブラウザの適当なところに貼り付けただけで、すべてがうまく行くわけではありません。 私は以前、なんかすべてを置き換える新しい形式だと思っていました。
WebAssemblyの使い方
使う手順
WebAssemblyを使う上での基本的な手順は以下のようになります
- WebAssemblyのバイナリファイルを作成
- ブラウザに読ませる普通のJS内にて1で作成したファイルを読み込む
- ブラウザ内部でコンパイル&ロード
- 普通のJSからWebAssemblyでエクスポートした関数を実行する
- 関数の返り値をJSで利用する
このような感じです。
やってみる!
前置きが長くなってしまいましたが、ここから実際にやっていこうと思います。
WebAssembyのバイナリファイル(.wasm)を生成する方法はC言語などからコンパイルするのが一般的です。 しかし、ここではWeb屋における試してみやすさを重視し、TypeScriptのサブセットでありWebAssemblyにコンパイル可能なAssemblyScript
を利用し、TypeScriptっぽいコードを書き、それをWebAssembyのバイナリへコンパイルすることにします。
必要なツールはnodeだけです!
あと、このブログでこれから書くコードはGithubで公開しています。
それではやっていきます。
サンプルのネタ
WebAssemblyのデモでは基本的に高度なグラフィックスやゲームなどが紹介されます、しかし今回手がかかるものはやれません。 簡単かつ大量計算を要するものとして、円周率計算 をネタとします。
円周率を求めるために、収束のあまり早くない、ライプニッツの公式
π/4 = ∑{(-1)k / (2k + 1)}
を利用してひたすら円周率を計算。 計算時間について JavaScriptオンリー と WebAssemblyとの合わせ技 で比較します。
JavaScriptオンリーの方
まずはじめにどんなことをWebAssemblyでやるのか、JavaScriptだけで全部実装したものを見てください。
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>javascript-calc-py</title> </head> <body> <h1>JavaScriptだけで円周率計算します!</h1> <p id="out">測定中...</p> <script type="module" src="executer.js"></script> </body> </html>
executer.js
これは円周率計算関数を何度も呼び出し、時間測定をし、かかる時間の平均と標準偏差を出しブラウザに表示します。
import { calcPy } from './py_calc.js' // 試行回数 const TIME = 200 const res = [] for (let i=0; i < TIME; i++) { // 実行 const startTime = new Date().getTime() const py = calcPy(100000000) // 10億項計算する const endTime = new Date().getTime() console.log(py) res.push(endTime - startTime) } // 平均, 標準偏差を計算 const sum = (array) => { return array.reduce((prev, crrent) => prev + crrent) } const avg = (array) => { return sum(array) / array.length } let avg_res = avg(res) let std_dev = Math.sqrt(sum(res.map(t => Math.pow(avg_res - t, 2))) / res.length) // 画面更新 document.getElementById("out").innerText = `平均${avg_res}(ms)かかりました! 標準偏差${std_dev}`
上のコードでimportしてるのが、円周率計算だけを別ファイルに出したpy_calc.js
です。 ライプニッツの公式で円周率を計算するコードです。 下に示します。
export function calcPy(times) { // ∑ (-1)^n / (2n+1) = π/4 let res = 0.0; let even = true; for (let i = 0; i < times; i++) { if (even) { res += 1 / (2 * i + 1); even = false; } else { res += -1 / (2 * i + 1); even = true; } } return res * 4; }
全体としてはindex.html
でexecuter.js
を読み込んで、そいつがpy_calc.js
をモジュールインポートして、エクスポートされた円周率関数を実行する流れ。
最近のブラウザならwebpackとかしなくてもimportとかが使えるですよ。(ちょっと古いネタだけど)
ちょっとした工夫点: 数式だと-1nだからMath.pow(-1,i) ってやりそうになるけど、我慢して分岐する。 i % 2 == 0 でもいいかとおもったけどこれよりbool値を直接入れたほうが早そうでこうした。
WebAssembyで円周率計算する!!
さて、どんな処理かわかったところで上のJavaScriptで円周率計算している部分をWebAssembly化しましょう。
パッケージインストールとか
まずAssemblyScriptをnpmから落とします。 これでasc
というtsc
っぽいAssembyScriptをコンパイルしてWebAssemblyのバイナリファイルを吐き出すコマンドが使えるようになります。
適当なディレクトリでpackage.jsonをこんなんで作ります。
{ "name": "assemblyscript-sample", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "asc -c ./tsconfig.json --asmjsFile -O -o py_calc.wasm py_calc.ts" }, "author": "namazu510", "license": "MIT", "dependencies": { "assemblyscript": "^0.3.0" } }
npm install
します。
tsconfig.json
を書いておきます。
{ "extends": "./node_modules/assemblyscript/tsconfig.assembly.json", "include": [ "./*.ts" ] }
これで環境はOKです。
AssembyScript書く => コンパイル
先程のpy_calc.js
と同様の処理をAssmblyScriptで記述します。
AssmblyScriptではTypeScriptの型に i32 とか i64 とか f64とかが追加されており、これで型を決められます。
py_calc.ts
export function calcPy(times: i32) : f64 { // ∑ (-1)^n / (2n+1) = π/4 let res: f64 = 0.0; let even: bool = true for (let i: i32 = 0; i < times; i++) { if (even) { res += 1 / (2 * i + 1); even = false; } else { res += -1 / (2 * i + 1); even = true; } } return res * 4; }
これをpy_calc.ts
という名でpackage.json
とかと同じディレクトリに置きます。
このAssemblyScriptをコンパイルしてWebAssemblyのバイナリを吐き出させます。 package.json
にスクリプト書いておいたので
npm run build
これでpy_calc.wasm
ができます。 AeemblyScriptをコンパイルしてwasmを作りましたが、C言語とかでこの計算内容を書いて、WebAssemblyコンパイラ通してもこのwasm拡張子のファイルが作れます。
出来上がるファイルはバイナリなので適当に見てみると.... なにもわからん(力が足りない)
バイナリ読み込んで実行
上でつくった.wasmのバイナリを読み込んで実行するJavaScriptを書きます。
javascriptオンリー版で使っていたexecuter.js
のインポートして計算関数を読んでいた部分について、WebAssemblyでエクスポートされた関数を呼ぶように変更しただけです。
executer.js
// 試行回数 const TIME = 200 const res = [] // バイナリを読み込み fetch('py_calc.wasm') .then(response => response.arrayBuffer()) .then(buffer => WebAssembly.compile(buffer)) // ブラウザコンパイル .then(module => WebAssembly.instantiate(module)) // インスタンス化 .then(instance => { // 試行! for(let i=0; i < TIME; i++) { const startTime = new Date().getTime() // assemblyでエクスポートした関数の実行 const py = instance.exports.calcPy(100000000) //10億項計算する const endTime = new Date().getTime() console.log(py) res.push(endTime - startTime) } // 平均, 標準偏差を計算 const sum = (array) => { return array.reduce((prev, crrent) => prev + crrent) } const avg = (array) => { return sum(array) / array.length } let avg_res = avg(res) let std_dev = Math.sqrt(sum(res.map(t => Math.pow(avg_res - t, 2))) / res.length) // 画面更新 document.getElementById("out").innerText = `平均${avg_res}(ms)かかりました! 標準偏差${std_dev}` })
このJavaScriptを読み込む単純なHTMLを用意します。
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>assemblyscript-calc-py</title> </head> <body> <h1>WebAssemblyで円周率計算します!</h1> <p id="out">測定中...</p> <script src="executer.js"></script> </body> </html>
このindex.html
を表示すれば円周率をライプニッツの公式で10億項まで計算を20回やってその平均と標準偏差を出してくれる。
完成!!!
すごいさくっと終わってしまいました。
実行結果&速度
それでは作ったJavaScriptOnly版とWebAssembly使用版の実行結果と速度を測ろうと思います。 私の手元のノートPCで測ります。 ブラウザはArchLinuxのgoogle-chrome-stableです。
JavaScriptOnly版
平均: 185.6ms
WebAssembly使用版
平均: 169.3ms
結果比較
jsでは185ms, WebAssmblyでは169msという結果に。 WebAssmbyで処理時間が91%になりました!!
jsも早いなぁという感想。 もうちょと題材を選ぶべきだったか? まぁWebAssmbly使ったら大規模計算が速くなったのは事実な気がする!! やったぁ!!
実行中CPUをTOPで見ていましたが、どちらの実装も1コアを全力で使い切っていました。
おわりに
WebAssemblyについてなんとなくは知っていたけど、実際に手を動かしてやってみるとどんなものかわかりました。
正直、現在の使い道はゲームやブラウザでマイニング(笑)とかに限られていて、私のような普通のフロントエンド屋にはまだ機が早いように感じます。 科学データとかのデータ可視化とかでは計算を要するので使うことがあるかも?とは感じる。 あとはCADとか物理シミュレーションしたりかなーって思います。
今後、DOMや各種APIが完全に対応とかなればwebpackが吐き出すものがWebAssemblyのバイナリがメインになる時代も来るのかなぁ?ってちょっと思いました。
さて今回はこの程度で。 田胡研究室アドベントカレンダー、明日はM1の@magrainさんです。 ご期待下さい!!