1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 10:22:23 +00:00
es6tutorial/docs/simd.md
2016-10-03 22:46:08 +08:00

15 KiB
Raw Blame History

SIMD 的用法

概述

SIMD发音/sim-dee/是“Single Instruction/Multiple Data”的缩写意为“单指令多数据”。它是JavaScript操作CPU对应指令的接口你可以看做这是一种不同的运算执行模式。与它相对的是SISD“Single Instruction/Single Data”即“单指令单数据”。

SIMD的含义是使用一个指令完成多个数据的运算SISD的含义是使用一个指令完成单个数据的运算这是JavaScript的默认运算模式。显而易见SIMD的执行效率要高于SISD所以被广泛用于3D图形运算、物理模拟等运算量超大的项目之中。

为了理解SIMD请看下面的例子。

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]

上面代码中,数组ab的对应成员相加,结果放入数组c。它的运算模式是依次处理每个数组成员一共有四个数组成员所以需要运算4次。

如果采用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]

上面代码之中,数组ab的四个成员的各自相加只用一条指令就完成了。因此速度比上一种写法提高了4倍。

一次SIMD运算可以处理多个数据这些数据被称为“通道”lane。上面代码中一次运算了四个数据因此就是四个通道。

SIMD通常用于矢量运算。

v + w	= v1, , vn+ w1, , wn
      = v1+w1, , vn+wn

上面代码中,vw是两个多元矢量。它们的加运算在SIMD下是一个指令、而不是n个指令完成的这就大大提高了效率。这对于3D动画、图像处理、信号处理、数值处理、加密等运算是非常重要的。比如Canvas的getImageData()会将图像文件读成一个二进制数组SIMD就很适合对于这种数组的处理。

总得来说SIMD是数据并行处理parallelism的一种手段可以加速一些运算密集型操作的速度。

数据类型

SIMD提供多种数据类型。

  • Float32x4四个32位浮点数
  • Float64x2两个64位浮点数
  • Int32x4四个32位整数
  • Int16x8八个16位整数
  • Int8x16十六个8位整数
  • Uint32x4四个无符号的32位整数
  • Uint16x8八个无符号的16位整数
  • Uint8x16十六个无符号的8位整数
  • Bool32x4四个32位布尔值
  • Bool16x8八个16位布尔值
  • Bool8x16十六个8位布尔值
  • Bool64x2两个64位布尔值

每种数据类型被x号分隔成两部分,后面的部分表示通道数,前面的部分表示每个通道的宽度和类型。比如,Float32x4就表示这个值有4个通道每个通道是一个32位浮点数。

每种数据类型都是一个方法,可以传入参数,生成对应的值。

var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);

上面代码中,变量a就是一个128位、包含四个32位浮点数即四个通道的值。

注意,这些数据类型方法都不是构造函数,前面不能加new,否则会报错。

var v = new SIMD.Float32x4(0, 1, 2, 3);
// TypeError: SIMD.Float32x4 is not a constructor

如果所有数据通道都是同样的值,可以使用splat方法生成值。

var v = SIMD.Float32x4.splat(0.0);

上面代码中,v的四个通道都是0.0

如果要取出单个通道的值,可以使用extractLane方法。

var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
var b = SIMD.Float32x4.extractLane(a, 0); // 1.0

上面代码中,extractLane方法的第一个参数是一个SIMD值第二个参数是通道的编号从0开始

如果要修改某个通道的值,可以使用replaceLane方法。

var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
var c = SIMD.Float32x4.replaceLane(a, 0, 5.0);

上面代码中经过替换后得到一个新的SIMD值(5.0, 2.0, 3.0, 4.0)。可以看到,replaceLane接受三个参数SIMD值、通道的编号从0开始、新的值。

方法:数学运算

每种数据类型都有一系列运算符,下面是其中的一些。

  • float32x4.abs(v):返回v的绝对值
  • float32x4.neg(v):返回v的绝对值的负值
  • float32x4.sqrt(v):返回v的平方根
  • float32x4.add(v, w)vw对应项的相加
  • float32x4.mul(v, w)vw对应项的相乘
  • float32x4.equal(v, w):比较vw对应项是否相等,返回的布尔值组成一个uint32x4的值

SIMD.%type%.add()

add方法接受两个SIMD值作为参数将它们的每个通道相加返回一个新的SIMD值。

var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
var b = SIMD.Float32x4(5.0, 10.0, 15.0, 20.0);
var c = SIMD.Float32x4.add(a, b);

上面代码中经过加法运算新的SIMD值为(6.0, 12.0, 18.0. 24.0)

SIMD.%type%.mul()

mul方法接受两个SIMD值作为参数将它们的每个通道相乘返回一个新的SIMD值。

var a = SIMD.Float32x4(-1, -2, 3, 4);
var b = SIMD.Float32x4(3, 3, 3, 3);
SIMD.Float32x4.mul(a, b);
// Float32x4[-3, -6, 9, 12]

SIMD.%type%.shiftLeftByScalar()

shiftLeftByScalar方法接受一个SIMD值作为参数然后将每个通道的值左移指定的位数返回一个新的SIMD值。

var a = SIMD.Int32x4(1, 2, 4, 8);
SIMD.Int32x4.shiftLeftByScalar(a, 1);
// Int32x4[2, 4, 8, 16]

如果左移后,新的值超出了当前数据类型的位数,溢出的部分会被丢弃。

var ix4 = SIMD.Int32x4(1, 2, 3, 4);
var jx4 = SIMD.Int32x4.shiftLeftByScalar(ix4, 32);
// Int32x4[0, 0, 0, 0]

SIMD.%type%.shiftRightByScalar()

shiftRightByScalar方法接受一个SIMD值作为参数然后将每个通道的值右移指定的位数返回一个新的SIMD值。

var a = SIMD.Int32x4(1, 2, 4, -8);
SIMD.Int32x4.shiftRightByScalar(a, 1);
// Int32x4[0, 1, 2, -4]

如果原来通道的值是带符号的值,则符号位保持不变,不受右移影响。如果是不带符号位的值,则右移后头部会补0

var a = SIMD.Uint32x4(1, 2, 4, -8);
SIMD.Uint32x4.shiftRightByScalar(a, 1);
// Uint32x4[0, 1, 2, 2147483644]

上面代码中,-8右移一位变成了2147483644是因为对于32位无符号整数来说-8的二进制形式是11111111111111111111111111111000,右移一位就变成了01111111111111111111111111111100,相当于2147483644

方法:通道处理

SIMD.%type%.load()

load方法用于从二进制数组读入数据生成一个新的SIMD值。

var a = new Int32Array([1,2,3,4,5,6,7,8]);
SIMD.Int32x4.load(a, 0);
// Int32x4[1, 2, 3, 4]

var b = new Int32Array([1,2,3,4,5,6,7,8]);
SIMD.Int32x4.load(a, 2);
// Int32x4[3, 4, 5, 6]

load方法接受两个参数一个二进制数组和开始读取的位置从0开始。如果位置不合法比如-1或者超出二进制数组的大小),就会抛出一个错误。

这个方法还有三个变种load1()load2()load3(),表示从指定位置开始,只加载一个通道、二个通道、三个通道的值。

// 格式
SIMD.Int32x4.load(tarray, index)
SIMD.Int32x4.load1(tarray, index)
SIMD.Int32x4.load2(tarray, index)
SIMD.Int32x4.load3(tarray, index)

// 实例
var a = new Int32Array([1,2,3,4,5,6,7,8]);
SIMD.Int32x4.load1(a, 0);
// Int32x4[1, 0, 0, 0]
SIMD.Int32x4.load2(a, 0);
// Int32x4[1, 2, 0, 0]
SIMD.Int32x4.load3(a, 0);
// Int32x4[1, 2, 3,0]

SIMD.%type%.splat()

splat方法返回一个新的SIMD值该值的所有通道都会设成同一个预先给定的值。

SIMD.Float32x4.splat(3);
// Float32x4[3, 3, 3, 3]
SIMD.Float64x2.splat(3);
// Float64x2[3, 3]

如果省略参数所有整数型的SIMD值都会设定0浮点型的SIMD值都会设成NaN

SIMD.%type%.swizzle()

swizzle方法返回一个新的SIMD值重新排列原有的SIMD值的通道顺序。

var t = SIMD.Float32x4(1, 2, 3, 4);
SIMD.Float32x4.swizzle(t, 1, 2, 0, 3);
// Float32x4[2,3,1,4]

上面代码中,swizzle方法的第一个参数是原有的SIMD值后面的参数对应将要返回的SIMD值的四个通道。它的意思是新的SIMD的四个通道依次是原来SIMD值的1号通道、2号通道、0号通道、3号通道。由于SIMD值最多可以有16个通道所以swizzle方法除了第一个参数以外最多还可以接受16个参数。

下面是另一个例子。

var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
// Float32x4[1.0, 2.0, 3.0, 4.0]

var b = SIMD.Float32x4.swizzle(a, 0, 0, 1, 1);
// Float32x4[1.0, 1.0, 2.0, 2.0]

var c = SIMD.Float32x4.swizzle(a, 3, 3, 3, 3);
// Float32x4[4.0, 4.0, 4.0, 4.0]

var d = SIMD.Float32x4.swizzle(a, 3, 2, 1, 0);
// Float32x4[4.0, 3.0, 2.0, 1.0]

SIMD.%type%.shuffle()

shuffle方法从两个SIMD值之中取出指定通道返回一个新的SIMD值。

var a = SIMD.Float32x4(1, 2, 3, 4);
var b = SIMD.Float32x4(5, 6, 7, 8);

SIMD.Float32x4.shuffle(a, b, 1, 5, 7, 2);
// Float32x4[2, 6, 8, 3]

上面代码中,ab一共有8个通道依次编号为0到7。shuffle根据编号取出相应的通道返回一个新的SIMD值。

方法:比较运算

SIMD.%type%.greaterThan()

greatThan方法用来比较两个SIMD值ab的每一个通道,如果在该通道中,a较大就得到true,否则得到false。最后所有通道的比较结果会组成一个新的SIMD值作为掩码返回。

var a = SIMD.Float32x4(1, 6, 3, 11);
var b = SIMD.Float32x4(1, 4, 7, 9);

var mask = SIMD.Float32x4.greaterThan(a,b);
// Bool32x4[false, true, false, true]

SIMD.%type%.lessThan()

lessThan方法用来比较两个SIMD值ab的每一个通道,如果在该通道中,a较小就得到true,否则得到false。最后所有通道的比较结果会组成一个新的SIMD值作为掩码返回。

var a = SIMD.Float32x4(1, 2, 3, 11);
var b = SIMD.Float32x4(1, 4, 7, 9);

var mask = SIMD.Float32x4.lessThan(a,b);
// Bool32x4[false, true, true, false]

SIMD.%type%.select()

select方法通过掩码生成一个新的SIMD值。它接受三个参数分别是掩码和两个SIMD值。

var a = SIMD.Float32x4(1, 2, 3, 4);
var b = SIMD.Float32x4(5, 6, 7, 8);

var mask = SIMD.Bool32x4(true, false, false, true);

SIMD.Float32x4.select(mask, a, b);
// Float32x4[1, 6, 7, 4]

上面代码中,select方法接受掩码和两个SIMD值作为参数。当某个通道对应的掩码为true会选择第一个SIMD值的对应通道否则选择第二个SIMD值的对应通道。

这个方法通常与比较运算符结合使用。

var a = SIMD.Float32x4(0, 12, 3, 4);
var b = SIMD.Float32x4(0, 6, 7, 50);

var mask = SIMD.Float32x4.lessThan(a,b);
// Bool32x4[false, false, true, true]

var result = SIMD.Float32x4.select(mask, a, b);
// Float32x4[0, 6, 3, 4]

上面代码中,先通过lessThan方法生成一个掩码,然后通过select方法生成一个由每个通道的较小值组成的新的SIMD值。

SIMD.%type%.allTrue()SIMD.%type%.anyTrue()

allTrue方法接受一个SIMD值作为参数然后返回一个布尔值表示该SIMD值的所有通道是否都为true

var a = SIMD.Bool32x4(true, true, true, true);
var b = SIMD.Bool32x4(true, false, true, true);

SIMD.Bool32x4.allTrue(a); // true
SIMD.Bool32x4.allTrue(b); // false

anyTrue方法则是只要有一个通道为true,就返回true,否则返回false

var a = SIMD.Bool32x4(false, false, false, false);
var b = SIMD.Bool32x4(false, false, true, false);

SIMD.Bool32x4.anyTrue(a); // false
SIMD.Bool32x4.anyTrue(b); // true

这两个方法通常与比较运算符结合使用。

var ax4    = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
var bx4    = SIMD.Float32x4(0.0, 6.0, 7.0, 8.0);
var ix4    = SIMD.Float32x4.lessThan(ax4, bx4);
var b1     = SIMD.Int32x4.allTrue(ix4); // false
var b2     = SIMD.Int32x4.anyTrue(ix4); // true

SIMD.%type%.min()SIMD.%type%.minNum()

min方法接受两个SIMD值作为参数将它们的每个通道的较小值组成一个新的SIMD值返回。

var a = SIMD.Float32x4(-1, -2, 3, 5.2);
var b = SIMD.Float32x4(0, -4, 6, 5.5);
SIMD.Float32x4.min(a, b);
// Float32x4[-1, -4, 3, 5.2]

如果有一个通道的值是NaN,则会返回NaN

var c = SIMD.Float64x2(NaN, Infinity)
var d = SIMD.Float64x2(1337, 42);
SIMD.Float64x2.min(c, d);
// Float64x2[NaN, 42]

minNum方法与min方法的作用一模一样,唯一的区别是如果有一个通道的值是NaN,则会优先返回另一个通道的值。

var ax4 = SIMD.Float32x4(1.0, 2.0, NaN, NaN);
var bx4 = SIMD.Float32x4(2.0, 1.0, 3.0, NaN);
var cx4 = SIMD.Float32x4.min(ax4, bx4);
// Float32x4[1.0, 1.0, NaN, NaN]
var dx4 = SIMD.Float32x4.minNum(ax4, bx4);
// Float32x4[1.0, 1.0, 3.0, NaN]

实例:求平均值

正常模式下,计算n个值的平均值,需要运算n次。

function average(list) {
  var n = list.length;
  var sum = 0.0;
  for (var i = 0; i < n; i++) {
    sum += list[i];
  }
  return sum / n;
}

使用SIMD可以将计算次数减少到n次的四分之一。

function average(list) {
  var n = list.length;
  var sum = SIMD.Float32x4.splat(0.0);
  for (var i = 0; i < n; i += 4) {
    sum = SIMD.Float32x4.add(
      sum,
      SIMD.Float32x4.load(list, i)
    );
  }
  var total = SIMD.Float32x4.extractLane(sum, 0) +
              SIMD.Float32x4.extractLane(sum, 1) +
              SIMD.Float32x4.extractLane(sum, 2) +
              SIMD.Float32x4.extractLane(sum, 3);
  return total / n;
}

上面代码先是每隔四位将所有的值读入一个SIMD然后立刻累加。然后得到累加值四个通道的总和再除以n就可以了。

二进制数组

SIMD可以与二进制数组结合生成数组实例。

var _f64x2 = new Float64Array(_f32x4.buffer);
var _i32x4 = new Int32Array(_f32x4.buffer);
var _i16x8 = new Int16Array(_f32x4.buffer);
var _i8x16 = new Int8Array(_f32x4.buffer);
var _ui32x4 = new Uint32Array(_f32x4.buffer);
var _ui16x8 = new Uint16Array(_f32x4.buffer);
var _ui8x16 = new Uint8Array(_f32x4.buffer);

下面是一个例子。

// a 和 b 是float32x4数组实例
function addArrays(a, b) {
  var c = new Float32x4Array(a.length);
  for (var i = 0; i < a.length; i++) {
    c[i] = SIMD.float32x4.add(a[i], b[i]);
  }
  return c;
}

参考链接