ECMAScript SIMD
SIMD 的含义是使用一个指令,完成多个数据的运算
SIMD
概述
SIMD(发音/sim-dee/
)是“Single Instruction/Multiple Data”的缩写,意为“单指令,多数据”。它是 JavaScript 操作 CPU 对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是 SISD(“Single Instruction/Single Data”),即“单指令,单数据”。
SIMD 的含义是使用一个指令,完成多个数据的运算;SISD 的含义是使用一个指令,完成单个数据的运算,这是 JavaScript 的默认运算模式。显而易见,SIMD 的执行效率要高于 SISD,所以被广泛用于 3D 图形运算、物理模拟等运算量超大的项目之中。
为了理解 SIMD,请看下面的例子。
上面代码中,数组a
和b
的对应成员相加,结果放入数组c
。它的运算模式是依次处理每个数组成员,一共有四个数组成员,所以需要运算 4 次。
如果采用 SIMD 模式,只要运算一次就够了。
上面代码之中,数组a
和b
的四个成员的各自相加,只用一条指令就完成了。因此,速度比上一种写法提高了 4 倍。
一次 SIMD 运算,可以处理多个数据,这些数据被称为“通道”(lane)。上面代码中,一次运算了四个数据,因此就是四个通道。
SIMD 通常用于矢量运算。
上面代码中,v
和w
是两个多元矢量。它们的加运算,在 SIMD 下是一个指令、而不是 n 个指令完成的,这就大大提高了效率。这对于 3D 动画、图像处理、信号处理、数值处理、加密等运算是非常重要的。比如,Canvas 的getImageData()
会将图像文件读成一个二进制数组,SIMD 就很适合对于这种数组的处理。
总的来说,SIMD 是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。将来与 WebAssembly 结合以后,可以让 JavaScript 达到二进制代码的运行速度。
数据类型
SIMD 提供 12 种数据类型,总长度都是 128 个二进制位。
- 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 位浮点数。
每个通道之中,可以放置四种数据。
- 浮点数(float,比如 1.0)
- 带符号的整数(Int,比如-1)
- 无符号的整数(Uint,比如 1)
- 布尔值(Bool,包含
true
和false
两种值)
每种 SIMD 的数据类型都是一个函数方法,可以传入参数,生成对应的值。
上面代码中,变量a
就是一个 128 位、包含四个 32 位浮点数(即四个通道)的值。
注意,这些数据类型方法都不是构造函数,前面不能加new
,否则会报错。
静态方法:数学运算
每种数据类型都有一系列运算符,支持基本的数学运算。
SIMD.%type%.abs(),SIMD.%type%.neg()
abs
方法接受一个 SIMD 值作为参数,将它的每个通道都转成绝对值,作为一个新的 SIMD 值返回。
neg
方法接受一个 SIMD 值作为参数,将它的每个通道都转成负值,作为一个新的 SIMD 值返回。
SIMD.%type%.add(),SIMD.%type%.addSaturate()
add
方法接受两个 SIMD 值作为参数,将它们的每个通道相加,作为一个新的 SIMD 值返回。
上面代码中,经过加法运算,新的 SIMD 值为(6.0, 12.0, 18.0. 24.0)
。
addSaturate
方法跟add
方法的作用相同,都是两个通道相加,但是溢出的处理不一致。对于add
方法,如果两个值相加发生溢出,溢出的二进制位会被丢弃; addSaturate
方法则是返回该数据类型的最大值。
上面代码中,Uint16
的最大值是 65535,Int16
的最大值是 32767。一旦发生溢出,就返回这两个值。
注意,Uint32x4
和Int32x4
这两种数据类型没有addSaturate
方法。
SIMD.%type%.sub(),SIMD.%type%.subSaturate()
sub
方法接受两个 SIMD 值作为参数,将它们的每个通道相减,作为一个新的 SIMD 值返回。
subSaturate
方法跟sub
方法的作用相同,都是两个通道相减,但是溢出的处理不一致。对于sub
方法,如果两个值相减发生溢出,溢出的二进制位会被丢弃; subSaturate
方法则是返回该数据类型的最小值。
上面代码中,Uint16
的最小值是0
,Int16
的最小值是-32678
。一旦运算发生溢出,就返回最小值。
SIMD.%type%.mul(),SIMD.%type%.div(),SIMD.%type%.sqrt()
mul
方法接受两个 SIMD 值作为参数,将它们的每个通道相乘,作为一个新的 SIMD 值返回。
div
方法接受两个 SIMD 值作为参数,将它们的每个通道相除,作为一个新的 SIMD 值返回。
sqrt
方法接受一个 SIMD 值作为参数,求出每个通道的平方根,作为一个新的 SIMD 值返回。
SIMD.%FloatType%.reciprocalApproximation(),SIMD.%type%.reciprocalSqrtApproximation()
reciprocalApproximation
方法接受一个 SIMD 值作为参数,求出每个通道的倒数(1 / x
),作为一个新的 SIMD 值返回。
reciprocalSqrtApproximation
方法接受一个 SIMD 值作为参数,求出每个通道的平方根的倒数(1 / (x^0.5)
),作为一个新的 SIMD 值返回。
注意,只有浮点数的数据类型才有这两个方法。
SIMD.%IntegerType%.shiftLeftByScalar()
shiftLeftByScalar
方法接受一个 SIMD 值作为参数,然后将每个通道的值左移指定的位数,作为一个新的 SIMD 值返回。
如果左移后,新的值超出了当前数据类型的位数,溢出的部分会被丢弃。
注意,只有整数的数据类型才有这个方法。
SIMD.%IntegerType%.shiftRightByScalar()
shiftRightByScalar
方法接受一个 SIMD 值作为参数,然后将每个通道的值右移指定的位数,返回一个新的 SIMD 值。
如果原来通道的值是带符号的值,则符号位保持不变,不受右移影响。如果是不带符号位的值,则右移后头部会补0
。
上面代码中,-8
右移一位变成了2147483644
,是因为对于 32 位无符号整数来说,-8
的二进制形式是11111111111111111111111111111000
,右移一位就变成了01111111111111111111111111111100
,相当于2147483644
。
注意,只有整数的数据类型才有这个方法。
静态方法:通道处理
SIMD.%type%.check()
check
方法用于检查一个值是否为当前类型的 SIMD 值。如果是的,就返回这个值,否则就报错。
SIMD.%type%.extractLane(),SIMD.%type%.replaceLane()
extractLane
方法用于返回给定通道的值。它接受两个参数,分别是 SIMD 值和通道编号。
replaceLane
方法用于替换指定通道的值,并返回一个新的 SIMD 值。它接受三个参数,分别是原来的 SIMD 值、通道编号和新的通道值。
SIMD.%type%.load()
load
方法用于从二进制数组读入数据,生成一个新的 SIMD 值。
load
方法接受两个参数:一个二进制数组和开始读取的位置(从 0 开始)。如果位置不合法(比如-1
或者超出二进制数组的大小),就会抛出一个错误。
这个方法还有三个变种load1()
、load2()
、load3()
,表示从指定位置开始,只加载一个通道、二个通道、三个通道的值。
SIMD.%type%.store()
store
方法用于将一个 SIMD 值,写入一个二进制数组。它接受三个参数,分别是二进制数组、开始写入的数组位置、SIMD 值。它返回写入值以后的二进制数组。
上面代码中,t1
是一个二进制数组,v1
是一个 SIMD 值,只有四个通道。所以写入t1
以后,只有前四个位置有值,后四个位置都是 0。而t2
是从 2 号位置开始写入,所以前两个位置和后两个位置都是 0。
这个方法还有三个变种store1()
、store2()
和store3()
,表示只写入一个通道、二个通道和三个通道的值。
SIMD.%type%.splat()
splat
方法返回一个新的 SIMD 值,该值的所有通道都会设成同一个预先给定的值。
如果省略参数,所有整数型的 SIMD 值都会设定0
,浮点型的 SIMD 值都会设成NaN
。
SIMD.%type%.swizzle()
swizzle
方法返回一个新的 SIMD 值,重新排列原有的 SIMD 值的通道顺序。
上面代码中,swizzle
方法的第一个参数是原有的 SIMD 值,后面的参数对应将要返回的 SIMD 值的四个通道。它的意思是新的 SIMD 的四个通道,依次是原来 SIMD 值的 1 号通道、2 号通道、0 号通道、3 号通道。由于 SIMD 值最多可以有 16 个通道,所以swizzle
方法除了第一个参数以外,最多还可以接受 16 个参数。
下面是另一个例子。
SIMD.%type%.shuffle()
shuffle
方法从两个 SIMD 值之中取出指定通道,返回一个新的 SIMD 值。
上面代码中,a
和b
一共有 8 个通道,依次编号为 0 到 7。shuffle
根据编号,取出相应的通道,返回一个新的 SIMD 值。
静态方法:比较运算
SIMD.%type%.equal(),SIMD.%type%.notEqual()
equal
方法用来比较两个 SIMD 值a
和b
的每一个通道,根据两者是否精确相等(a === b
),得到一个布尔值。最后,所有通道的比较结果,组成一个新的 SIMD 值,作为掩码返回。notEqual
方法则是比较两个通道是否不相等(a !== b
)。
SIMD.%type%.greaterThan(),SIMD.%type%.greaterThanOrEqual()
greatThan
方法用来比较两个 SIMD 值a
和b
的每一个通道,如果在该通道中,a
较大就得到true
,否则得到false
。最后,所有通道的比较结果,组成一个新的 SIMD 值,作为掩码返回。greaterThanOrEqual
则是比较a
是否大于等于b
。
SIMD.%type%.lessThan(),SIMD.%type%.lessThanOrEqual()
lessThan
方法用来比较两个 SIMD 值a
和b
的每一个通道,如果在该通道中,a
较小就得到true
,否则得到false
。最后,所有通道的比较结果,会组成一个新的 SIMD 值,作为掩码返回。lessThanOrEqual
方法则是比较a
是否等于b
。
SIMD.%type%.select()
select
方法通过掩码生成一个新的 SIMD 值。它接受三个参数,分别是掩码和两个 SIMD 值。
上面代码中,select
方法接受掩码和两个 SIMD 值作为参数。当某个通道对应的掩码为true
时,会选择第一个 SIMD 值的对应通道,否则选择第二个 SIMD 值的对应通道。
这个方法通常与比较运算符结合使用。
上面代码中,先通过lessThan
方法生成一个掩码,然后通过select
方法生成一个由每个通道的较小值组成的新的 SIMD 值。
SIMD.%BooleanType%.allTrue(),SIMD.%BooleanType%.anyTrue()
allTrue
方法接受一个 SIMD 值作为参数,然后返回一个布尔值,表示该 SIMD 值的所有通道是否都为true
。
anyTrue
方法则是只要有一个通道为true
,就返回true
,否则返回false
。
注意,只有四种布尔值数据类型(Bool32x4
、Bool16x8
、Bool8x16
、Bool64x2
)才有这两个方法。
这两个方法通常与比较运算符结合使用。
SIMD.%type%.min(),SIMD.%type%.minNum()
min
方法接受两个 SIMD 值作为参数,将两者的对应通道的较小值,组成一个新的 SIMD 值返回。
如果有一个通道的值是NaN
,则会优先返回NaN
。
minNum
方法与min
的作用一模一样,唯一的区别是如果有一个通道的值是NaN
,则会优先返回另一个通道的值。
SIMD.%type%.max(),SIMD.%type%.maxNum()
max
方法接受两个 SIMD 值作为参数,将两者的对应通道的较大值,组成一个新的 SIMD 值返回。
如果有一个通道的值是NaN
,则会优先返回NaN
。
maxNum
方法与max
的作用一模一样,唯一的区别是如果有一个通道的值是NaN
,则会优先返回另一个通道的值。
静态方法:位运算
SIMD.%type%.and(),SIMD.%type%.or(),SIMD.%type%.xor(),SIMD.%type%.not()
and
方法接受两个 SIMD 值作为参数,返回两者对应的通道进行二进制AND
运算(&
)后得到的新的 SIMD 值。
上面代码中,以通道0
为例,1
的二进制形式是0001
,5
的二进制形式是01001
,所以进行AND
运算以后,得到0001
。
or
方法接受两个 SIMD 值作为参数,返回两者对应的通道进行二进制OR
运算(|
)后得到的新的 SIMD 值。
xor
方法接受两个 SIMD 值作为参数,返回两者对应的通道进行二进制“异或”运算(^
)后得到的新的 SIMD 值。
not
方法接受一个 SIMD 值作为参数,返回每个通道进行二进制“否”运算(~
)后得到的新的 SIMD 值。
上面代码中,1
的否运算之所以得到-2
,是因为在计算机内部,负数采用”2 的补码“这种形式进行表示。也就是说,整数n
的负数形式-n
,是对每一个二进制位取反以后,再加上 1。因此,直接取反就相当于负数形式再减去 1,比如1
的负数形式是-1
,再减去 1,就得到了-2
。
静态方法:数据类型转换
SIMD 提供以下方法,用来将一种数据类型转为另一种数据类型。
SIMD.%type%.fromFloat32x4()
SIMD.%type%.fromFloat32x4Bits()
SIMD.%type%.fromFloat64x2Bits()
SIMD.%type%.fromInt32x4()
SIMD.%type%.fromInt32x4Bits()
SIMD.%type%.fromInt16x8Bits()
SIMD.%type%.fromInt8x16Bits()
SIMD.%type%.fromUint32x4()
SIMD.%type%.fromUint32x4Bits()
SIMD.%type%.fromUint16x8Bits()
SIMD.%type%.fromUint8x16Bits()
带有Bits
后缀的方法,会原封不动地将二进制位拷贝到新的数据类型;不带后缀的方法,则会进行数据类型转换。
上面代码中,fromFloat32x4
是将浮点数转为整数,然后存入新的数据类型;fromFloat32x4Bits
则是将二进制位原封不动地拷贝进入新的数据类型,然后进行解读。
Bits
后缀的方法,还可以用于通道数目不对等的拷贝。
上面代码中,原始 SIMD 值t
是 4 通道的,而目标值是 8 通道的。
如果数据转换时,原通道的数据大小,超过了目标通道的最大宽度,就会报错。
实例方法
SIMD.%type%.prototype.toString()
toString
方法返回一个 SIMD 值的字符串形式。
实例:求平均值
正常模式下,计算n
个值的平均值,需要运算n
次。
使用 SIMD,可以将计算次数减少到n
次的四分之一。
上面代码先是每隔四位,将所有的值读入一个 SIMD,然后立刻累加。然后,得到累加值四个通道的总和,再除以n
就可以了。