SIMD types

试验性的JavaScript SIMD API 介绍了矢量类型,它可以在支持的CPU上使用SIMD/SSE指令;SIMD是Single Instruction/Multiple Data的缩写。SIMD操作使用单个指令操作多组数据。标量操作(SISD)每个指令只会操作一组数据。
 
使用单指令处理数据集会极大的提升应用的性能。具体的提升效果取决于你的数据集(矢量对象、封装数据)的大小或是其他参数。因此,SIMD指令早已广泛应用于3D图像处理、音视频、物理计算、加密及其它领域。
 

关于SIMD的缺点,这也是为何算法要针对SIMD进行设计。当你的算法要分别处理不同的数据时,数据集中的不同数据并不能被区分处理。在后面的文章中我们将知道如何使用蒙板以及如何将数据重新对齐来解决这一问题。

共享内存或进程优先级排列允许全面的并行处理(MIMD)。使用这些技术可以更轻松的为进程排序,但这些概念同样可以使用在SIMD上。例如,你可以想象在你程序中的每个进程中都是用SIMD指令。这个由Peter Jensen (Intel)制作的Mandelbrot示例,说明了SIMD和Web Workers带来的性能提升。

SIMD在JavaScript中的应用

JavaScript SIMD API 包含几种新的数据类型操作指令,允许你通过JavaScript使用SIMD指令。浏览器根据底层硬件针对这些API为用户提供了经过高度优化的方法。现在,JS SIMD API特别针对 ARMv7 platforms with NEONx86 platforms with SSE进行设计。

让我们来看一个SIMD的数据类型,例如SIMD.Float32x4。一个SIMD矢量对象包涵大量的单元数据,它们被称为通道。一个针对当前API的SIMD寄存器宽度为128bit。当一个矢量的宽度是4(X4)时,寄存器内有4个Float32数据,它们的通道名为x,y,z,w。现在,相比于对每个对象操作4次,SIMD允许你同时对四个通道进行操作。

在下列模型中,只使用了一个指令(加),从而数据可以被SIMD处理:

SISD SIMD

模型1&2:SISD和SIMD的比较

标量 / SSID 代码示范 (不使用任何循环,只是一种演示):

var a = [1, 2, 3, 4];
var b = [5, 6, 7, 8];
var c = [];
c[0] = a[0] + b[0];
c[1] = a[1] + b[1];
c[2] = a[2] + b[2];
c[3] = a[3] + b[3];
c; // Array[6, 8, 10, 12]

现在使用 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]

这将同时将四个通道内的值进行加法,并返回一个新的执行加法后的SIMD Float32数据。

正如你所在这三行SIMD代码中看到的,一组JS方法允许你建立一组数据并允许你执行矢量指令(这里是加法)。在写这篇文章时,还没有实行操作符重载(例如“+”符号)来简化SIMD代码的书写。然而,JavaScript SIMD API至今并未完成,加入运算符重载是下一个草案中的计划之一。你可以在ecmascript_simd GitHub repository关注这份计划的发展。

重定位数据以适合SIMD矢量对象

通常会有数组作为SIMD矢量对象的输入量。然而,这些数组的结构也许并不总是适合SIMD操作。比如让我们看一下图片RGBA颜色数据。

通过canvas中 CanvasRenderingContext2D.getImageData() 方法和 ImageData.data 原型方法,你可以获取一个RGBA格式包含图片底层像素数据的一元数组,内部数据数值为0~255的整数(包含边界):

[R, G, B, A, R, G, B, A, R, G, B, A, ...]

 如果我们想现在处理一下这个图像, 比如使用公式Y = 0.299r + 0.587g + 0.114b来计算感知亮度/对比度, 我们需要为SIMD重新构建数据。用SIMD处理r,g,b的不同权重值时,我们需要将每种颜色拆分成适合的格式。看起来像是这样:

[R, R, R, R, R, R, ...] * 0.299 +
[G, G, G, G, G, G, ...] * 0.587 +
[B, B, B, B, B, B, ...] * 0.114b =
[Y, Y, Y, Y, Y, Y, ...]

并行条件分支

在纯量代码中, 进程分支通常通过状态来控制。示例如下:

var a = [1, 2, 3, 4];
var b = [5, 6, 7, 8];
var c = [];
for (var i = 0; i < 4; i++) {
  if (a[i] < 3) {
    c[i] = a[i] * b[i];
  } else {
    c[i] = b[i] + a[i];
  }
}
console.log(c); // [5, 12, 10, 12]

我们不想为SIMD矢量对象中的每个分支分别写指令并分别执行,为SIMD使用选择器蒙板会带来更高的效率。

分支, 蒙板, 选择

 SIMD.%type%.select() 方法从一个选择器蒙板中区分通道 。它允许你用分支来对SIMD对象类型内的数据进行操作。下图中,一个自定义的蒙板选择器从SIMD矢量变量a,b中选择数据:

蒙板的返回数据可以是几组对照方法中的任意一个。你也可以用 SIMD.%type%.bool()方法来创建自定义的选择器蒙板。

用这项技术我们可以重写上一个代码示例中的标量分支,使用select()方法并行执行加法和乘法操作:

var a = SIMD.Float32x4(1, 2, 3, 4);
var b = SIMD.Float32x4(5, 6, 7, 8);
var mask = SIMD.Float32x4.lessThan(a, SIMD.Float32x4.splat(3));
// Int32x4[-1, -1, 0, 0]
var result = SIMD.Float32x4.select(mask,
                                   SIMD.Float32x4.mul(a, b),
                                   SIMD.Float32x4.add(a, b));
console.log(result); // Float32x4[5, 12, 10, 12]

在此版本SIMD的示例中,数据被再次放入SIMD矢量中。之后,为了依照某种状态创建分支,需使用SIMD.Float32x4.lessThan() 方法。 它根据本组对照内通道的真值来返回一个Boolean类型的选择器蒙板。第一次对比是矢量,第二次对比由splat 方法创建,它将全部四条通道设置为3。这使这个判断与标量方法中(a[i] < 3)等效。

若要从选择器蒙板中获取向量结果,需要使用select 方法。它需要三个参数:第一个参数是蒙板,第二个参数是真值表。如果选择器蒙板通道为真,通道对应的值将会从真值表中获取。若不是,通道值将会从第三个参数,非真值表(?)中获取。

更多SIMD算法及用例

通常来熟,一组数据如果可以用相同的指令来处理,那么用SIMD可以获取更高的效率。以下算法及用例配合SIMD指令可以获得极佳的发挥:

SIMD在JavaScript中的现状

SIMD API 已可以在近期版本的 Firefox Nightly 中使用. 你同样可以参考 "in development" in Microsoft Edge 和 "Intent to implement" in Blink/Chromium.

SIMD现在仍未成为官方标准甚至草案中的一部分。然而就算如此,Intel, Google, Mozilla 和其它组织已经令其受ECMAScript 7/2016和开发建议支持。

一个基于typed arrays的Polyfill参考实现已在ecmascript_simd Github库中出现。

参考

文档标签和贡献者