スプーキーズのちょっとTech。

SPOOKIES社内のより技工的な、専門的なブログページです。

JavaScriptを高速化する5つの手法

こんにちは、モリタです。

JSを高速化 する

f:id:moritanian:20170926151527p:plain:w160

nativeに比べて遅いと言われるjsですがパフォーマンスチューニングによりずいぶん改善しているようです。しかしながらWebGLなどまだまだ高速化が必要な分野も数多くあります。そこで、今回はjavascriptを高速化する手法を簡単に見ていきたいと思います。

Typed Array

動的な配列であるjsの通常の配列に対し、固定長の配列であるTypedArrayが用意されています。低機能ですが、各要素に高速でアクセスする事ができます。以下のようにまずサイズを決めてバッファを確保してからTypedArrayを生成します。 canvasの getImageData () で取得できる画像データもTypedArrayのようですね。

var buffer = new ArrayBuffer(8); // 8byteのサイズのバッファ確保
var arr   = new Int32Array(buffer); // 32bit(4byte)のint配列
console.log(arr); // [0, 0]

WebAssembly

動的型付け言語であるJsは実行時に型チェックや型変換が行われ、それがネイティブに比べて遅くなる原因のひとつになっていました。そこでMozillaからasm.jsという手法が提案されました。asm.jsでは全ての変数が静的な型付けを行います。TypedArrayの考え方をさらに推し進めたものと言えるでしょう。

このasm.jsに対して各ブラウザベンダーが協力して策定しているバイナリフォーマットの規格がwebAssemblyです。 webAssemblyを利用するにはc, c++ コードをEmscriptenツールによって.wasmバイナリに変換するのが一般的なようです。  各ブラウザの実装状況は以下のようにデスクトップではほぼすべてのブラウザ最新で実装されています。 f:id:moritanian:20170926114309p:plain

また、unityのwebGLビルドでもオプション設定でwebAssemblyを使用できるようです。

WebWorker

jsは本来シングルスレッドで動作しますが、webWorkerを利用することでマルチスレッド動作を実現できます。 以下のように new Worker("ファイル名") で実行することができます。 また、postMessage() , onmessage() でスレッド間でデータをやり取りできます。

main.js

var myWorker = new Worker("worker.js"); // worker.js  を別スレッドで実行
 myWorker.postMessage([first.value,second.value]); // Sending message as an array to the worker
myWorker.onmessage = function(e) {
        result.textContent = e.data;
        console.log('Message received from worker');
    };

worker.js

onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
  console.log('Posting message back to main script');
  postMessage(workerResult);
}

参考

github.com

gpgpu

gpgpuとは本来画像処理に使われるgpuの計算資源を画像処理以外の演算に使うことです。gpuは行列計算など計算密度が大き(条件分岐などが少ない)く、並列度が大きい演算に関してCPUに比べて性能を発揮します。最近は機械学習が利用例としてホットですね。 gpgpuを利用するために以下のライブラリがあります。 gpu.rocks

github.com

gpu.jsでは以下のように記述できます。

const gpu = new GPU();
const multiplyMatrix = gpu.createKernel(function(a, b) {
  var sum = 0;
  for (var i = 0; i < 512; i++) {
    sum += a[this.thread.y][i] * b[i][this.thread.x];
  }
  return sum;
}).setOutput([512, 512]);

const c = multiplyMatrix(a, b);

Simd

こちらも並列化により高速化を実現する試みの一種で命令セットレベルの並列化を実現します。 以下ではベクトル演算を並列化しています。

var a = SIMD.Float32x4(1, 2, 3, 4);
var b = SIMD.Float32x4(5, 6, 7, 8);
var c = SIMD.Float32x4.add(a,b); // Float32x4[6,8,10,12]

残念ながらEcmascriptでまだドラフト段階のためリリース版ブラウザの実装はまだのようです。

番外編

javasciptで演算子オーバーロードしたい

高速化とは全く関係ありませんがみなさんも一度はjsでも演算子オーバーロードしたいと思ったことがあるはずです。いや、そんなことはないですね。 少し気になったので調べてみたのですが結論としては、 【Javascript】演算子をオーバーロードしたい話 - Qiita にある通り普通のやり方では実現できずコードを構文解析して演算子オーバーロードしている箇所を書き換える必要があるようです。 そこで実際に実装してライブラリ化してみました!

GitHub - moritanian/minamike.js: このライブラリに過度な期待はしないでください

f:id:moritanian:20170926124329p:plain

このライブラリでCのstd::cout 風に記述するとこのようになります

const cout = "std::cout";
String.prototype.__operators__ = {
    "<<" : function(s1, s2){
        console.log(s2);
    }
  };
 
 
 cout << "Hello World!";

まあ、全く需要なさそうですが。

それではよきjsライフを!