SIMD.jsで画像処理してみたよ


# やったこと
[SIMD.js](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SIMD)でどのくらい高速化できるのか、簡単な画像処理で試してみました。
やることは単純で、Canvasから取得したRGBAをYUVに変換した後にガンマ補正を掛け、YUVからRGBAに戻すだけです。ガンマ補正はLUTを当てるだけでSIMDに乗せられないので、RGBA/YUV変換がSIMD化の対象となりますが、とりあえずRGBAからYUVへの変換だけSIMD化しました。

Windows 10 + [Firefox Nightly](https://nightly.mozilla.org/) 47.0a1で動作確認しています。Edgeでも設定を変えるとSIMDが使えるのですが、ビットシフトに対応していないようなので動かないです。

ソース: [https://github.com/arenahito/simdjs-example](https://github.com/arenahito/simdjs-example)
デモ: [http://arenahito.github.io/simdjs-example/](http://arenahito.github.io/simdjs-example/)

# SIMD化前
まずは、SIMDなしで作ります。
RGBAからYUVに変換するだけですが、YUVを加工しやすいようにPlaner形式としています。RGBA→RGBA→……からYYY……→UUU……→VVV……となります。
あと、Alphaはいらないので捨てています。YUVからRGBAに戻すときは固定値を設定する感じで。

```js
function convertRgbaToYuv(rgba, width, height, output) {
  var imageSize = width * height;

  for (var y = 0; y < height; y++) {
    var rgbaOffsetY = y * width * 4;
    var yuvOffsetBaseY = y * width;

    for (var x = 0; x < width; x++) {
      var rgbaOffset = rgbaOffsetY + x * 4;
      var yuvOffsetBase = yuvOffsetBaseY + x;

      var r = rgba[rgbaOffset + 0];
      var g = rgba[rgbaOffset + 1];
      var b = rgba[rgbaOffset + 2];
      output[yuvOffsetBase + imageSize * 0] = (306 * r + 601 * g + 117 * b) >> 10;
      output[yuvOffsetBase + imageSize * 1] = (-173 * r - 339 * g + 512 * b + 131072) >> 10;
      output[yuvOffsetBase + imageSize * 2] = (512 * r - 429 * g - 83 * b + 131072) >> 10;
    }
  }
}
```

# SIMD化後
SIMD化してみます。
Int32x4にRGBの3バイトを乗せてYUVに変換します。1バイト分余っている上に、3並列だと性能に期待できないんじゃ・・・とか言ってはいけません。
RGB→Y、RGB→U、RGB→Vと計算するのでは無く、RGBそれぞれについて計算した後に足し込みます。こうしないと加算処理にSIMDを使えないので。

```js
function convertRgbaToYuvSimd(rgba, width, height, output) {
  var imageSize = width * height;
  var arrayYUV = new Int32Array(4);

  var vR0 = SIMD.Int32x4(306, -173, 512, 0);
  var vG0 = SIMD.Int32x4(601, -339, -429, 0);
  var vB0 = SIMD.Int32x4(117, 512, -83, 0);
  var vUVOffset = SIMD.Int32x4(0, 131072, 131072, 0);

  for (var y = 0; y < height; y++) {
    var rgbaOffsetY = y * width * 4;
    var yuvOffsetBaseY = y * width;

    for (var x = 0; x < width; x++) {
      var rgbaOffset = rgbaOffsetY + x * 4;
      var yuvOffsetBase = yuvOffsetBaseY + x;

      var r = rgba[rgbaOffset + 0];
      var g = rgba[rgbaOffset + 1];
      var b = rgba[rgbaOffset + 2];

      var vR1 = SIMD.Int32x4.mul(SIMD.Int32x4.splat(r), vR0);
      var vG1 = SIMD.Int32x4.mul(SIMD.Int32x4.splat(g), vG0);
      var vB1 = SIMD.Int32x4.mul(SIMD.Int32x4.splat(b), vB0);

      var vRGB1 = SIMD.Int32x4.add(vR1, vG1);
      var vRGB2 = SIMD.Int32x4.add(vRGB1, vB1);
      var vRGB3 = SIMD.Int32x4.add(vRGB2, vUVOffset);
      var vRGB4 = SIMD.Int32x4.shiftRightByScalar(vRGB3, 10);

      SIMD.Int32x4.store(arrayYUV, 0, vRGB4);
      output[yuvOffsetBase + imageSize * 0] = arrayYUV[0];
      output[yuvOffsetBase + imageSize * 1] = arrayYUV[1];
      output[yuvOffsetBase + imageSize * 2] = arrayYUV[2];
    }
  }
}
```

# 試してみて
RGBAからYUVに10回変換するのに掛かった時間を測ってみました。SIMDなし145ms、SIMDあり170ms。まあ、あまり効率的にSIMD化出来てないですし・・・。

処理性能はおいておくとして、そもそもサポートされている機能が貧弱なので使いづらいです。SSEとかのIntel系のは使ったことが無いのでわかりませんが、[Neon](http://www.arm.com/ja/products/processors/technologies/neon.php)と同じくらいの機能は欲しいなと。