2019.09.27

サーバサイドはWebAssemblyの夢を見るか? – Node.jsでwasmってみた

はじめに

こんにちは。次世代システム研究室のS.T.です。

Webの技術に興味のある方は「WebAssembly(wasm)」というキーワードを聞いたことがあるかもしれません。2019年に入ってからGoogle Earthのwasm実装のベータ版がリリースされたり、ディープな勉強会が開催されたり、密かな盛り上がりを見せている技術です。特に「Assembly」という語が入っているだけで私のような低レイヤ好きには非常に魅力的なものに見えます(実態はあまりAssembly感がないですが……)。

しかし、ゲームや動画など重量級のコンテンツを扱わないWebサイトでは活用できる機会はなかなかありません。さらに、Google V8に代表される高速なJavascriptエンジンの存在により、wasm導入によるパフォーマンス向上の効果とメンテナンスコスト増加のトレードオフを考えると、適用できる場面も限られてくるかと思います。特に、繰り返し呼び出されるような処理ではJavascriptエンジンの最適化によりネイティブに匹敵するパフォーマンスが出る場合もあり、wasmを使えばパフォーマンスが上がると言い切れる状況ではなくなっています。

そこで少し視点を変えて、サーバサイドアプリケーションでwasmを使うサンプルを紹介します。

おさらい

本題に入る前に、wasmについて本記事で必要な範囲でおさらいします。「wasmが何者であるか」については既に様々な解説記事が執筆されているため、詳細は割愛します。
  • wasmはブラウザやNode.jsなどで高速に実行可能なコード
  • Javascriptからwasmモジュールを呼び出して利用できる
  • C/C++やRustなどの言語をwasmにコンパイル可能
  • サンドボックス化された環境で実行

サーバサイドでwasm

今回注目したいのは、Node.jsでブラウザと同じwasmモジュールが利用可能な点です。つまり、サーバサイドとクライアントサイドで同じwasmモジュールを呼び出すことで、完全に同一の処理を行うことが可能になります。例えばサーバ側にデータを送りつつクライアント側で処理をして、その結果を先に表示して見かけ上の遅延を減らしたり、双方の処理結果を比較して不正なリクエストを防いだりといった利用方法が考えられます。

そこで、今回はWebアプリケーションのサーバサイドとクライアントサイドで同じコードを実行する実験を行います。Node.js、Expressで構成したWebアプリから、「C言語で記述してwasmにコンパイルした同一モジュール」をサーバ・クライアントの双方から呼び出し、計算結果と実行時間を計測します。比較のため、(浮動小数点の計算を意識しない)同じ処理をJavascriptで記述し、同一の計測を行います。コンパイルにはEmscriptenを使用します。検証にはUbuntu 18.04を使用しています。

サンプルアプリケーション

wasmファイルの作成

#include <emscripten/emscripten.h>

double EMSCRIPTEN_KEEPALIVE bake_pi(void) {
  double res = 0;
  for (int i = 0; i < 2000000000; i++) {
    if (i % 2) {
      res -= (1.0 / (i * 2 + 1));
    } else {
      res += (1.0 / (i * 2 + 1));
    }
  }

  return res * 4;
}
今回はグレゴリーライプニッツ級数を計算して円周率を求めるプログラムをwasmにしてみます。emccでコンパイルをしますが、今回は出力を.wasmファイルのみとし、ロードやインスタンス作成の処理も自分で実装することにします。

Node.jsからwasmの関数を呼び出す

メインのロジックにwasmで実装した関数を用いたAPIを作成します。

var bytes = new Uint8Array(fs.readFileSync('./public/wasm/test.wasm'));
var mod = new WebAssembly.Instance(new WebAssembly.Module(bytes), {});
今回はサーバ起動時に同期的にインスタンスを作成しています。作成した関数は mod.exports._bake_pi() で呼び出しが可能です。計算結果をJSONで返却します。

ブラウザからwasmの関数を呼び出す

今回はブラウザでwasm実行、wasm実装API呼び出し、Javascriptで同じ計算する実装のそれぞれをWebWorkerで実装し、同時に実行します。インスタンスの作成自体はNode.js版とほぼ同じですが、Promiseを使った非同期処理を使用しています。

fetch('/wasm/test.wasm').then(response =>
  response.arrayBuffer()
).then(bytes =>
  WebAssembly.instantiate(bytes, {})
).then(obj => {
  mod = obj.instance;
  self.postMessage(0);
});
Node.jsのAPI呼び出しやJavascriptでの計算は特に変わった処理はせず、axiosでGETしたり、C言語と同じロジックでループしたりしています。実行時間の計測には performance.now() で取得した時間の差を使っています。

実行結果

作成したアプリケーションをローカルのVMで実行してブラウザからアクセスしてみます。検証環境のスペックは以下のとおりです。

 
サーバ クライアント
OS / ブラウザ Ubuntu 18.04 LTS Windows 10 / Chrome
CPU 2vCPU local VM Intel Core i5-7500 (4 Core/4 Thread)
Memory 2GB 16GB
 

以下のような実行結果が得られました。左側が計算された円周率、右側が実行時間です。wasm版はクライアントとサーバサイドで同じ結果が出ていますね。


クライアントサイドでwasmを動かしたものが最も高速、次いでサーバサイドでwasmを動かしたAPI、最後がクライアントサイドのJavascriptという結果です。実行時間についてはみなさんが予想していた通りの結果なのではないでしょうか?Javascript実装のみ計算結果が違うのは、浮動小数点の精度を意識した実装をしていないためです。

このように、wasmを使うとサーバサイドとクライアントサイドで細かい部分まで同一な処理を簡単に実現できます。最終的に後続処理に使う値はサーバサイドで計算したものだとしても、フロントエンドで高速に計算して得られた仮の結果でアニメーションなどを描画することで、ユーザ体験を向上させることができます。

おわりに

今回紹介したように、wasmはブラウザ以外でも実行できるため、サーバサイドアプリケーションにも適用することができます。一方、このような使い方が有効な場面というのは少なく、やはりwasmの出番は限られてしまいます。しかし、Wasmerのようにスタンドアロンでwasmのコードを実行する環境の開発も進んでいます。C/C++やRustで記述したコードから、様々なプラットフォームで動作し、安全性の高いバイナリを得ることができるのは大きな強みです。

wasmの実行環境や関連プロジェクトは日々進化していますし、今後利用事例なども増えてくるかと思いますので、興味のある方は一緒にウォッチしていきましょう!

 

最後に、次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

 

参考

MDN WebAssembly https://developer.mozilla.org/ja/docs/WebAssembly

 

Pocket

関連記事