JavaScript
ECMAScript
SIMD
esnext
SIMD.js
More than 1 year has passed since last update.

はじめに

これはなに

JavaScriptで複数のデータを一度に扱ってより高速に処理を行うための新機能(データ型とAPI)です。

1回の命令で複数のデータを並列に処理できるSIMDという手法があります。大抵のCPUは拡張命令としてSIMDに対応しています(MMX, SSE, NEON, etc...)。 SIMD.js はJavaScriptからそのSIMDの拡張命令を使おうというものです。

新しいデータ型「SIMD型」

具体的には、新しいプリミティブなデータ型として「SIMD型」というものが定義されます。

SIMD型の例
var x = SIMD.Float32x4(0.1, 1.2, 3.2, 3.2);
console.log(typeof x);  //"float32x4"

新しく追加されるSIMD型は次の通りです。

ES.nextで追加される予定のSIMD型
SIMD.Int8x16
SIMD.Int16x8
SIMD.Int32x4
SIMD.Uint8x16
SIMD.Uint16x8
SIMD.Uint32x4
SIMD.Float32x4
SIMD.Bool8x16
SIMD.Bool16x8
SIMD.Bool32x4

SIMD型はこれまでのJavaScriptで例えるなら、「要素数が決まっているTypedArrayのようなもの」です。

例えば、SIMD.Int8x168 bit のsigned integerが 16個 並んだものです。名前の最後の数字が"要素"数(SIMD型では「レーン」と呼びます」)になります。

SIMD型は各数値のビット数とレーン数を掛けた合計が必ず「128」になります。SIMD.jsはこの128-bit単位でまとめる(制限する)ことで、CPUのSIMD拡張命令を生かして並列に処理を行います。この同時に扱うデータサイズが固定であるところがただの配列1であるTypedArrayと異なるところです。

データ型 128-bit SIMDレジスタ 内の各レーン
Int8x16
Uint8x16
s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15
Int16x8
Uint16x8
s0 s1 s2 s3 s4 s5 s6 s7
Int32x4
Uint32x4
Float32x4
s0 s1 s2 s4

なお、SIMDブーリアン型(例:SIMD.Bool32x4)は、32-bitのブーリアン型が4つ並んでる(!)…ように一瞬見えますが、これはあくまでも 128bit単位で処理するための中間値(an intermediate value)表現であるとされています。この場合、SIMD.Int32x4の各レーンに対しての処理を真偽値で振り分ける時などに使います。

SIMDオブジェクト

SIMD型の頭についているSIMD.SIMDオブジェクトという新しい標準組み込みグローバルオブジェクトです。各それぞれのデータ型をプロパティとして持つだけで、Mathオブジェクトと同様、newしたりする事は出来ません。

typeof SIMD;    //object

Object.getOwnPropertyNames(SIMD);
/*
[ 'Float32x4',
  'Int32x4',
  'Bool32x4',
  'Int16x8',
  'Bool16x8',
  'Int8x16',
  'Bool8x16',
  'Uint32x4',
  'Uint16x8',
  'Uint8x16' ]
*/

Intlオブジェクトを除くとこれまでJavaScriptの標準オブジェクトにはこのような名前空間を分けるようなものはありませんでした。その意味で、SIMDオブジェクトは初の標準組み込み名前空間用オブジェクトと言えるかもしれません。

ES2015のTypedArrayはこのように名前空間で分ける事をしなかったため、データ型の数だけ似たような名前の標準組み込みオブジェクトがグローバルなスコープに散乱することになり、残念なことになっていたのでこれはES.nextの地味な進化といえるでしょう。

早くなる?

扱うデータサイズは限定されますが、一度の処理で4~16個扱えるので単純計算で4~16倍早くなります。

例えば要素数16のUint8Arrayの各要素に対して処理を行うとき、これまでループで16回繰り返し処理を行う必要があった所、SIMD.Uint8x16型なら1回の処理でまとめて行えます。

また、配列のように繋がったデータとしてではなく並列に並んだデータを扱えると考えると、3D計算であるようなVector3のようなベクトル型の行列演算などにも使えます。

一方で既にJSの実行エンジンが内部的に最適化でSIMD命令を使用していた場合は変わりません。また、まとめて処理できるといっても大量のベクトル演算は WebGLでGPGPU or WebCL に敵いません。

あくまでも、機械に頼らず人力でJavaScriptを書く中で、CPU & シングルスレッドの範囲で効率よくなるというものです。

扱いの制限

普通の演算子は使えない

SIMD型はプリミティブなデータ型ではありますが、普通の数値のように+*などの演算子で計算できません。add()mul()といった関数で行う事になります。

NG
var a = SIMD.Int32x4.splat(0) + SIMD.Int32x4.splat(1);
//                            ^
//
//TypeError: Cannot convert a SIMD value to a number
OK
var a = SIMD.Int32x4.add(SIMD.Int32x4.splat(0), SIMD.Int32x4.splat(1));

ここで長くてタイピングが面倒だからと普通にデータを抜き出して+などの演算子で処理してしまうと、CPUのSIMD拡張命令を経由しないので高速化の恩恵が受けられません。この制限は算術演算、ビット演算、比較演算、型のキャストなどに及びます2

既存のループコードをSIMD.jsで置き換える場合、この制限が理由で結果的に遅くなるアルゴリズムのコード3を書いてしまう可能性があります。

SIMD命令に非対応の環境がある

実行環境のCPUがSIMD命令を持たない場合、当然ながら高速化の恩恵に預かれません。PC用ではさすがにもう無いと思いますが、古めのモバイル用のCPU(Tegra2とか)やIoT用途のCPUでは対応していないということがあり得ます。

仕様書にはCPUが対応していない時どうしろ、とは書いていないのでJSの実行系にまかされる事になると思われます。

TypedArrayとの関係

「要素数が決まっているTypedArrayのようなもの」

と上記で書きましたが、決してTypedArrayを置き換えるものではありません。むしろ組み合わせて使う事が前提ともいえるAPIになっています。

SIMD型の各レーンの最大値は32-bit float pointです4が、JavaScriptのnumberは64-bit float pointです。そのため、大きな数のnumberを何も考えずにレーンに突っ込むと桁溢れを起こしてしまいます。

SIMD型はデータ型の意識なしに扱うのは難しい形ですので、データ型を適切に扱えるTypedArray型との連携が必要になります。

実装状況とpolyfill

proposalのgithubリポジトリにあるpolyfillを使用するのが現時点では無難です。

手元のNode.js v5.3では、--harmony_simdオプションを付ける事で有効化されましたが、
SIMD.Uint32x4,SIMD.Uint16x8,SIMD.Uint8x16といったデータ型が未実装、実装済みデータ型のオブジェクトでもstore/load系関数が未実装でした。

SIMD.jsを触ってみようとする時の注意点

  • npmモジュールで公開されているpolyfillは現在の仕様に合っていない古いものです
  • MDNのドキュメントは、英語版も古い提案仕様に基づいています
    • ざっくり理解には役立ちますが、APIの説明は参考にならないものがあります
  • ES7 compat tableは記事執筆時点で最新仕様(v0.9.1)に追従しきれていません

  1. Arrayではなく、Cで言うところの配列。 

  2. それぞれの演算用の関数が用意されています。 

  3. 既存のループ回数を単に1/4とかするとループ回数に余りがでますが、それを以前と同じ処理にするのか、新たに処理を書くのか、、、といった所に罠がありそうです。 

  4. 策定途中ではSIMD.Float64x2というのも検討されていたようですが、 phase 2 に先送りされたようです。