はじめに
- JavaScript stage 0,1,2,3 Advent Calendar 2015 の記事です
- SIMD.jsを紹介します
- 記事執筆時で stage3 の仕様です
これはなに
JavaScriptで複数のデータを一度に扱ってより高速に処理を行うための新機能(データ型とAPI)です。
1回の命令で複数のデータを並列に処理できるSIMDという手法があります。大抵のCPUは拡張命令としてSIMDに対応しています(MMX, SSE, NEON, etc...)。 SIMD.js はJavaScriptからそのSIMDの拡張命令を使おうというものです。
新しいデータ型「SIMD型」
具体的には、新しいプリミティブなデータ型として「SIMD型」というものが定義されます。
var x = SIMD.Float32x4(0.1, 1.2, 3.2, 3.2);
console.log(typeof x); //"float32x4"
新しく追加される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.Int8x16
は 8 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()
といった関数で行う事になります。
var a = SIMD.Int32x4.splat(0) + SIMD.Int32x4.splat(1);
// ^
//
//TypeError: Cannot convert a SIMD value to a number
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は現在の仕様に合っていない古いものです
- 公式のpolyfillをDLして使いましょう
- MDNのドキュメントは、英語版も古い提案仕様に基づいています
- ざっくり理解には役立ちますが、APIの説明は参考にならないものがあります
- ES7 compat tableは記事執筆時点で最新仕様(v0.9.1)に追従しきれていません