JavaScriptでC/C++コードを実行してネイティブアプリのように高速にする [WebAssembly]
HTML5の「WebAssembly」を使用するとJavaScriptの実行速度をネイティブアプリのように高速にできます。主な対象は画像、音声、動画処理などの重たい演算処理です。
重たい処理をC/C++やRustのコードで作成後にEmscriptenやWebAssembly Studioで中間言語(*.wasm)にコンパイルします。
[WebAssembly Studio]
ブラウザでページが表示される時にJavaScriptの指示で中間言語を更にコンパイルして実行します。JavaScriptとはメモリの共有で連携が取れます。
ちなみに私はWebAssemblyの事をインラインアセンブラのように「インラインC言語」と呼んでいます。また、最新技術なのでIE11は動作しませんがモダンなChrome/FireFox/Edgeなどは対応しています。
WebAssemblyの使い方は後述します。
どれくらい早いかを確認する
MP3/OGG/AAC/FLAC/WAVなどの音声ファイルをWave形式に変換。
Waveファイルの各種操作。
例として、JavaScriptのみで約4分のWaveファイル(34.8MB)を操作すると約2000msの処理時間でした。今回のWebAssemblyを使用すると処理時間が約130msへとなりました。約1/15ですので約15倍、速くなってます。
※WebAssemblyの使い方によっては、もっと早くなる場合があります。
1. WebAssemblyの使い方
JavaScriptでWebAssemblyを利用するには中間言語(*.wasm)を作成できる開発環境が必要です。私のおススメはブラウザで動作する「WebAssembly Studio」です。
表示したら「Empty C Project」を選択して「Create」を押します。
基本的な操作は、各ファイルを編集したら右上にある「Save」でファイルを一つ一つ保存します。(まとめて保存できない)
「Build & Run」でビルドと実行です。プロジェクトが完成したら「Download」します。「out¥main.wasm」が中間言語のファイルです。
2. 共有メモリのサンプルコード(C言語版)
C言語側で「共有メモリ」を生成するサンプルです。
main.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> body { background-color: rgb(255, 255, 255); } </style> </head> <body> <span id="container"></span> <script src="./main.js"></script> </body> </html>
main.c
今回はrawがJavaScriptからも利用できる共有メモリとなります。
#define WASM_EXPORT __attribute__((visibility("default"))) unsigned char raw[100]; WASM_EXPORT unsigned char* getAddress() { return &raw[0]; } WASM_EXPORT int test() { raw[0] = 3; raw[1] = 4; raw[2] = 5; return raw[7]; }
JavaScriptから呼び出す関数の上には「WASM_EXPORT」を記述します。
main.js
バッファと開始アドレスを指定してUint8Arrayを生成しています。
// WebAssembly fetch("../out/main.wasm").then(function(response) { // バイナリ return response.arrayBuffer(); }).then(function(bytes) { // コンパイル return WebAssembly.compile(bytes); }).then(function(module) { // インスタンス化 return WebAssembly.instantiate(module); }).then(function(instance) { // バッファ var buffer = instance.exports.memory.buffer; // C言語のgetAddress()で開始アドレスを取得する var offset = instance.exports.getAddress(); var list =new Uint8Array(buffer, offset, 100); list[4] = 123; list[5] = 1; list[6] = 2; list[7] = 3; // C言語のtest()を実行する var result =instance.exports.test(); console.log(list); console.log(result) adocument.getElementById("container").innerHTML = result; });
実行結果
何をやったかはC言語とJavaScriptのソースを見てください。
配列サイズを増やすと中間言語(*.wasm)のサイズが肥大するので、昔ながらの4096単位でメモリアクセスすると良いと思います。
その配列の実体は動的メモリ確保の「ヒープ領域」ではなく、グローバル変数を格納する「データ領域」で中間言語の中に確保されるようです。
次章のJavaScript側で共有メモリを生成すると、ファイルサイズは肥大しません。ただ、C言語の共有メモリの方が速い気がします。
3. 共有メモリのサンプルコード(JavaScript版)
JavaScript側で「共有メモリ」を生成するサンプルです。
main.html
C言語版と同じです。
main.c
#define WASM_EXPORT __attribute__((visibility("default"))) WASM_EXPORT int test(unsigned char* raw, int offset) { raw[offset + 0] = 3; raw[offset + 1] = 4; raw[offset + 2] = 5; return raw[offset+ + 7]; }
main.js
// WebAssembly fetch("../out/main.wasm").then(function(response) { // バイナリ return response.arrayBuffer(); }).then(function(bytes) { // コンパイル return WebAssembly.compile(bytes); }).then(function(module) { // インスタンス化 return WebAssembly.instantiate(module); }).then(function(instance) { console.log("初期メモリ" + instance.exports.memory.buffer.byteLength); var offset = instance.exports.memory.buffer.byteLength; // 1ページ分(65536バイト)のメモリを確保する instance.exports.memory.grow(1); console.log("現在のメモリ" + instance.exports.memory.buffer.byteLength); var list = new Uint8Array(instance.exports.memory.buffer,offset,65536); list[4] = 123; list[5] = 1; list[6] = 2; list[7] = 3; // C言語のtest()を実行する var result =instance.exports.test(instance.exports.memory.buffer,offset); console.log(list); console.log(result) document.getElementById("container").innerHTML = result; });
実行結果
結果はC言語版と同様です。
その他のソースを見たい方は私の WAVE.js (WebAssembly未使用)と WAVE.wasm.js (WebAssembly使用)を見比べてみてください。
前述のWAVE.wasm.jsのバージョンは「v1.01」でJavaScriptで共有メモリを生成しています。wasmのファイルサイズは8.91 KB。音声形式の変換で使用しているWAVE.wasm.jsは「v1.00」でC言語で共有メモリを生成しています。wasmのファイルサイズは67.4 KB。
v1.00では4096単位でメモリを操作。v1.01では一気にメモリを操作しています。何故、v1.00を使用しているかというと、そのWebアプリの処理ではv1.01より高速だからです。
昔ながらの「4096単位のメモリアクセス」または「C言語で生成した共有メモリ」のどちらかが高速化に影響を与えていると思われます。
4. 補足
JavaScript側の配列はInt8Array、Uint8Array、Uint8ClampedArray、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Arrayがあります。C言語側はそれらと同様な型を指定すればOKです。
5. 注意事項
ローカルでは実行できません。サーバーにアップしてから実行します。
fetch()でwasmファイルを読み込むとデフォルトではキャッシュに保存されます。なので、wasmファイルを書き換えてテストする際にはキャッシュを毎回、削除するか「fetch("main.wasm",{cache:'no-cache'}).then...」のように「no-cache」の属性を付与して下さい。
GlobalFetch.fetch() (MDN)
6. WebAssemblyの調査結果など
C言語でmalloc()やfree()はどうやら使えないようです。
とコンソールで怒られます。過去の情報ではmod._malloc()、mod.HEAP8.set()などで動的メモリを確保/設定できたようですが、現在はそのようなものはなさそうです。
意外だったのが、浮動小数点同士の乗算はC言語よりJavaScriptの方が速いです。また、JavaScriptはファイルの読み込みは早いですが、書き込みが遅いようです。
以上となります。
参考リンク
WebAssembly (MDN)
関連記事
前の記事: | JPEGのExif情報の確認と削除 [EXIF.js] |
次の記事: | Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>? [Reactのエラー] |