WebAssemblyをより簡単にWebアプリケーションに適用する方法

WebAssemblyをより簡単にWebアプリケーションに適用する方法を紹介し、JavaScriptとWebAssemblyの簡単なパフォーマンステストの結果を共有したいと思います。

世の中には、多くのプログラミング言語があり、さまざまな面で長所と短所があります。開発者によって意見が異なるでしょうが、パフォーマンスの面では、C/C++> Java> JavaScript、生産性の面では、JavaScript> Java> C/C++が有力なようです。JavaScriptの世界ではJIT(Just-In-Time)コンパイラが登場し、パフォーマンスが飛躍的な発展を遂げ、Webアプリケーションの規模がますます大きくなる礎となりました。にもかかわらず依然としてパフォーマンスに対する渇きは解消されておらず、WebAssemblyが登場しました。

パフォーマンスにおける最も大きな違いは、コンパイルをいつ行うか(コンパイル言語とインタプリタ言語)、実行可能なコードがAssemblyに直接実行できるか、中間コードを実行するVM(Virtual Machine)があるかどうかです。JavaScriptは柔軟性と生産性の利点がある一方、ランタイムに解釈されてコンパイルや実行がなされるため、パフォーマンスにおいては諦めなければならない部分があります。

WebAssemblyに変換できる方法

既に作成しているFirefoxで駆動していたasm.jsコード、C/C++コードを以下のようなツールチェーンを使ってWebAssemblyモジュールに変換できます。

  • asm.jsで作成しWebAssemblyに変換する(binaryen
  • C/C++で作成しWebAssemblyに変換する(Emscripten

しかし、フロントエンドの開発者が高速パフォーマンスを得るためにWebAssemblyモジュールを使うには、あまりにも遠い道のりになるでしょう。make、llvm、C/C++、Compile、Linkingなどの不慣れな過程を、WebAssemblyに昇華させ、作成したモジュールをWebpackやRollupなどの開発環境に統合させる過程が必要です。

AssemblyScript

WebAssemblyに変換できる言語の条件は、変数のタイプを確認できる言語でなければならないという特徴があります。C/C++のRust、TypeScript(WebAssemblyコンパイルターゲットの追加に対する議論)などの言語がWebAssemblyに変換できますが、Typescriptのサブセットである「AssemblyScript」やWebAssemblyにも変換できます。AssemblyScriptを選択した理由は使いやすさです。NPMに登録されており、フロントエンドの開発者がより簡単にWebAssemblyコードをテストできるからです。

ここではAssemblyScriptを使ってWebAssemblyモジュールに変換してみます。
AssemblyScriptでWebAssemblyを作成する際の注意事項は以下のとおりです。

  • 暗示的なタイプ変換を防ぐためにタイプを明示すること
  • デフォルトパラメータは、デフォルト値の初期化が必要
  • 明確なタイプのみサポートする(anyやundefinedはサポートしない)
  • 論理演算子&&と|| の結果は、常にbool値を意味する

assemblyscriptをNPMにインストールすると、コマンドラインから簡単にコンパイルして.wasm(WebAssembly)ファイルを作成できます。また、生成されたWASMファイルをモジュールとして使用できるassemblyscript-loaderwasm-loaderもあり、前者は、コード内でWASMモジュールをロードすることができ、オプションが多く、後者はWebpackローダーで提供され、より簡単に使用ができ、バンドルも可能であるという違いがあります。

AssemblyScriptをもっと使いやすくしよう

C/C++のようなネイティブ言語からコンパイルしてWebAssemblyを使用するには、あまりにも手続きが複雑です。お馴染みのJavaScript言語系で作成してWebAssemblyにコンパイルし、Webpackを使ってバンドルできるようにしよう。

assemblyscript-live-loader

前に紹介した2つのNPMパッケージを使うと簡単に利用できますが、Webpack開発環境でJavaScriptコードを作成し、バンドルされているように使用するには、それぞれ少し不足している部分があり、Webpackローダーを直接作成しました。ローダーは2つの機能をサポートします。

  • AssemblyScriptをWASMにコンパイル
  • WASMモジュールをWebAssembly.Moduleで使用できるようにバンドル

注:wasm-loaderを使用する場合、uglifyingにエラーがあり、WASMロード部分は直接作成しました。

パッケージのインストール

まだNPMに登録していないので、GitHubからインストールします。

npm install --save-dev https://github.com/dongsik-yoo/assemblyscript-live-loader.git

Webpack Loader config

WebpackにAssemblyScriptで作成したファイルをバンドルできるように設定を追加します。
webpack.config.js

module: {
    loaders: [
        {
            test: /\.asc$/, // assemblyscriptソースファイル
            exclude: '/node_modules/',
            loader: 'assemblyscript-live-loader'
        }
    ]
}

WebAssemblyでコンパイルするコードをAssemblyScriptで作成

./asc/Calculator.ascファイルを作成し、以下の内容を作成します。

export function add(a: int, b: int): int {
    return a + b;
}

export function subtract(a: int, b: int): int {
    return a - b;
}

export function multiply(a: int, b: int): int {
    return a * b;
}

export function divide(a: int, b: int): int {
    return a / b;
}

モジュールのインポート

そうすると、複雑に見えるコンパイルプロセスとモジュールの読み込み、バンドルのプロセスがWebpackローダーを用いて簡単に行われます。
index.jsに使うWebAssemblyモジュールを呼び出して作成したテスト関数を呼び出し、結果を受け取ることができます。

import Calculator from './asc/Calculator.asc';

const calc = new Calculator().exports;
const add = calc.add(44, 8832);
const subtract = calc.subtract(100, 20);
const multiply = calc.multiply(13, 4);
const divide = calc.divide(20, 4);

console.log(add);
console.log(subtract);
console.log(multiply);
console.log(divide);

Build

npx webpack

注:NPM 5.2から追加されたnpxを使用すると便利

JavascriptとWebAssemblyのパフォーマンステスト

簡単な四則演算とfactorialをJavaScriptとAssemblyScriptをコンパイルしたWebAssemblyバージョンで作成してパフォーマンス測定を行いました。ところが、WebAssemblyのパフォーマンス測定結果は、期待するものではありませんでした。
注:ChromeとFirefoxはWebAssemblyを基本適用してテストできます。

テスト環境

  • テストツール:Chrome 59.0.3071.115、Firefox 54.0.1、micro-benchmark
  • テスト結果チャート:TUI-Chart 2.9.0
  • テスト演算:加算、減算、乗算、除算、factorial
  • 凡例:JavaScript(赤)、WebAssembly(オレンジ)、バーグラフが低いほど高速パフォーマンスを示す
  • テストページ:上記のテストは、ここから確認できる
  • テストコード:テストコードは、JavascriptWebAssemblybenchmark駆動コードで確認できる

テストコードのサンプル

// 駆動コード
const factorialNumber = 1000;
const factorialLoop = 10000;
const N = 1000000;
...

// Javascript, ループはJavaScriptから駆動
{
    name: 'Factorial',
    fn: function () {
        var i = 0;
        for (; i < factorialLoop; i += 1) {
            CalculatorJS.factorial(factorialNumber);
        }
    }
},
// Javascript, ループはテスト関数内で駆動
{
    name: 'Factorial',
    fn: function () {
        CalculatorJS.factorialWithLoopCount(factorialLoop, factorialNumber);
    }
},
// WebAssembly, ループはJavaScriptから駆動
{
    name: 'Factorial',
    fn: function () {
        var i = 0;
        for (; i < factorialLoop; i += 1) {
            CalculatorWASM.factorial(factorialNumber);
        }
    }
},
// WebAssembly, ループはテスト関数内で駆動
{
    name: 'Factorial',
    fn: function () {
        CalculatorWASM.factorialWithLoopCount(factorialLoop, factorialNumber);
    }
}
// Javascript Test Code
function factorial(num) {
    let tmp = num;

    if (num < 0) {
        return -1;
    } else if (num === 0) {
        return 1;
    }

    while (num > 2) {
        tmp *= num;
        num -= 1;
    }

    return tmp;
}

function factorialWithLoopCount(count, num) {
    let i = 0;
    for (; i < count; i += 1) {
        factorial(num);
    }
}
// WebAssembly Test Code
export function factorial(num: int): int {
    var tmp: int = num;

    if (num < 0) {
        return -1;
    } else if (num === 0) {
        return 1;
    }

    while (num > 2) {
        tmp *= num;
        num -= 1;
    }

    return tmp;
}

export function factorialWithLoopCount(count: int, num: int) {
    var i: int = 0;
    for (; i < count; i += 1) {
        factorial(num);
    }
}

Chromeテスト結果

Firefoxテスト結果

テスト結果とまとめ

WebAssemblyはJavaScriptに置き換わるものではなく、JavaScriptよりも速く駆動できるモジュールをWebAssemblyロールを通じてパフォーマンスを向上させる代替方式になるでしょう。JavaScriptのパフォーマンスを左右する要素はあまりにも多いからです。調査するまではWebAssemblyが断然速いと思いましたが、factorialを除く四則演算の性能は、JavaScriptがより速く測定されました。パフォーマンスに影響を与える要素はたくさんありますが、いくつか列挙すると、

  • JavaScriptエンジンの性能
  • JITのパフォーマンス最適化
  • コールスタックの処理性能(例: 再帰関数を使う場合)
  • WebAssembly関数を呼び出すのにかかる時間
  • WebAssemblyコンパイラの最適化

このテストでは反復的なループを実行しました。簡単な四則演算ではJITで反復的なコードがAssemblyにコンパイルされ、むしろより速かったのではないかと思います。また所謂「トランポリン(trampolining)」と呼ばれるJavaScriptからWebAssemblyへコンテキストを切り替える過程でかなりの性能を削って食べているようです。ChromeとFirefox間でも差が大きいですね。

おまけ

WebAssemblyモジュールでエラーが発生した場合、どのようデバッグするか検討しましたが、幸いなことにChromeでコールスタックを下記のように取得できます。

Reference

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop