ホーム > カテゴリ > HTML5・JavaScript >

JavaScriptでC/C++コードを実行してネイティブアプリのように高速にする [WebAssembly]

HTML5の「WebAssembly」を使用するとJavaScriptの実行速度をネイティブアプリのように高速にできます。主な対象は画像、音声、動画処理などの重たい演算処理です。

重たい処理をC/C++やRustのコードで作成後にEmscriptenWebAssembly 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」です。

https://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()はどうやら使えないようです。

Uncaught (in promise) TypeError: WebAssembly Instantiation: Imports argument must be present and must be an object

とコンソールで怒られます。過去の情報ではmod._malloc()、mod.HEAP8.set()などで動的メモリを確保/設定できたようですが、現在はそのようなものはなさそうです。

意外だったのが、浮動小数点同士の乗算はC言語よりJavaScriptの方が速いです。また、JavaScriptはファイルの読み込みは早いですが、書き込みが遅いようです。

以上となります。

参考リンク

WebAssembly (MDN)





関連記事



公開日:2019年03月13日 最終更新日:2024年01月30日
記事NO:02743