Dart学习笔记(38):SIMD单指令流多数据流

发表于2018-07-04 01:36 阅读(43)

利用单指令流多数据流(Single Instruction Multiple Data,简称SIMD)指令集,Dart中编写程序可以使用新的数值类型。通过SIMD数值类型Float32x4,程序可以同时操作4个浮点数,对于3D图形、图像处理、音频处理、以及其它数值计算的算法,可能有400%的速度提升。

本文地址:http://www.cndartlang.com/967.html

本文将介绍如何使用dart:typed_data库提供的SIMD数值类型:Float32x4和Int32x4。这两种类型都同时保存了4个数值,并且同时对4个数进行操作。Int32x4非常有限,仅对比较、分支和选择有用。Float32x4提供了标准的算术操作集,以及更多。

1、性能的提升

这取决于涉及的算法,SIMD指令可能使数值计算获得150%或更多的速度提升。如图所示,速度提升最大的,往往是在使用4×4矩阵乘法的时候。

3D图形应用中,每帧作4×4变换矩阵乘法多次。通过使用Float32x4替代double,可以提升4×4矩阵乘法超过300%的速度。下面的视频重点介绍了一个骨骼动画Demo,SIMD版本动画的性能几乎是非SIMD版本的400%。

超清请查看Bilibili:http://www.bilibili.com/video/av6543237/

机器学习算法(如自动语音识别)使用了高斯混合模型(GMM),也因使用了SIMD而获益。使用Float32x4替换double,是通过一个GMM实现的速度的两倍

2、支持情况一览

如下表所示,尽管你所有的代码,可能都使用了dart:typed_data的API,如Float32x4,但你的代码可能并没有变快。如果在运行时环境,类型并没有被加速,那么性能等于或低于类似的非向量化代码。

IA32/X64
ARM
JavaScript
支持
Yes
Yes
Yes
加速
Yes
如果包含NEON

3、SIMD的思维方式

想象一下,一个SIMD值有四个通道,并且每个通道都包含了一个标量值。通道按水平方向进行组织,分别被命名为xyzw。注意,w是第四通道。

如下图所示,SIMD值之间按垂直方向进行操作。例如,(1.0, 2.0, 3.0, 4.0)和(5.0, 6.0, 7.0, 8.0)相加的结果为(6.0, 8.0, 10.0, 12.0)。

请记住,所有的四个加法运行时同时进行(并行)的。

尽管事实上,Float32x4有4个通道保存不同的浮点数,你仍然不应该简单的将Float32x4作为可以保存浮点数、并分别进行读取的列表。相反,应该把Float32x4想作一个不可变对象。

Float32x4或Int32x4内部在读写各个通道的时候,是进行水平操作。例如,求各个通道值的总和(很慢,应避免此操作)。如果你并不能完全避免水平操作,那么调整代码以尽可能少的执行操作。

因为在SIMD值上面执行的操作会影响四个通道,所以你存储在Float32x4中的数据应该保持一致。例如,像素的alpha通道值。如果是非一致数据,一个像素将是red、green、blue、alpha四个值。

细想一下更改图像alpha值的算法。每个像素被表示为4个浮点数,分别表示red、green、blue、alpha值。如果Float32x4保存非一致数据,如下图所示,那么你将更改所有的Float32x4,而不能高效地、在不更改red、green和blue值的情况下更改alpha值。
非一致数据(糟糕的!)

与此相反,如果使用Float32x4保存4个像素的一致数据时,如下图所示:
一致数据(合理的!)

单个的Float32x4操作就可以调整alpha,而不需要更改所有的Float32x4的red、green或blue通道值。

当Float32x4保存一致数据时,你不必特意处理一个通道(这会很慢)。组织你的数据保持一致,往往说起来比做起来更容易,但它可以获得更高的性能。

4、类型

dart:typed_data库有四种类型支持SIMD:Float32x4、Int32x4、Float32x4List和Int32x4List。

Float32x4中的每个通道保存了一个单精度(32位)浮点数。本文中大多实例使用的也是Float32x4。

Int32x4中的每个通道保存的则是一个无符号的32位整数值。Int32x4并不支持算术操作,而是在逻辑运算中使用,例如比较和选择操作。

你可以显式的创建Int32x4对象,也可以从Float32x4方法的返回值获得(下面的分支章节会说明)。创建一个显式的选择器蒙版(Mask)时,可以使用构造函数Int32x4.bool()。

Int32x4.bool(bool x, bool y, bool z, bool w);

该构造函数创建了一个新的Int32x4实例,其中布尔参数为true时,设置值为0xFFFFFFFF,布尔参数为false时值为0x0。

当你需要Float32x4对象的列表时,尽可能使用Float32x4List而不是List<Float32x4>。后面实例章节有使用Float32x4List的示例代码。

5、常用技术

本节会讲解一些常见任务的代码。

5.1 执行算术操作

Float32x4的算术操作与Dart中double或int的算术操作没有什么区别。例如:

var a = new Float32x4(1.0, 2.0, 3.0, 4.0);
var b = new Float32x4(5.0, 6.0, 7.0, 8.0);
var sum = a + b;

5.2 分别读取通道值

警告:读取各个通道非常慢。

你可以使用x、y、z和w的getter,分别读取通道值。例如:

double addXY(Float32x4 v) {
  return v.x + v.y;
}

5.3 分别写入通道值

警告:该操作非常慢。

记住,所有Float32x4和Int32x4的实例是不可变的,因此你不能修改指定通道值。然而,你可以使用已存在实例的通道值构造一个新的实例,期间修改某通道的值。例如:

Float32x4 v = ...;
v = v.withX(x); // 修改v中x通道的值

5.4 打乱或重新排序

你可以打乱Float32x4实例中值的顺序,但并不是使用类似下面的糟糕的代码:

Float32x4 v = ...;
double x = v.x;
double y = v.y;
double z = v.z;
double w = v.w;
Float32x4 v2 = new Float32x4(w, z, y, x); // v中通道值的倒序

而是简单地使用多个字段中的一个,按指定顺序使用通道值,返回新的对象:

Float32x4 v2 = v.shuffle(Float32x4.WZYX);  // v中通道值的倒序

建议的方法不仅使代码更容易阅读,而且性能更好。

5.5 分支

当编写分支代码的时候,需要特别注意。细想下面的代码片段:

Float32x4 a = ...;
Float32x4 b = ...;
Float32x4 c;

if (a > b) {
  c = a;
} else {
  c = b;
}

它有一个问题:如果a中的通道值只有部分大于b,而其余的值不大于呢?两个Float32x4实例的比较结果不能换算成单一的布尔值。正因如此,Float32x4不支持标准的比较运算符。相反,它定义了以下方法:

Int32x4 greaterThan(Float32x4 other);
Int32x4 greaterThanOrEqual(Float32x4 other);
Int32x4 lessThan(Float32x4 other);
Int32x4 lessThanOrEqual(Float32x4 other);
Int32x4 equal(Float32x4 other);
Int32x4 notEqual(Float32x4 other);

每个方法返回一个Int32x4对象,比较为true时,通道值为0xFFFFFFFF。比较为false时,通道值为0x0。该Int32x4被称为选择器蒙版,用于一个通道接一个通道,从两个Float32x4中选择相应的值。下面是重写的SIMD换算的代码片段:

Float32x4 a = ...;
Float32x4 b = ...;
Int32x4 mask = a.greaterThan(b);  // 创建选择器蒙版
Float32x4 c = mask.select(a, b);   // 选择

select()方法在Int32x4类中的定义如下:

Float32x4 select(Float32x4 trueValue, Float32x4 falseValue);

如果蒙版中的通道值为0xFFFFFFFF,结果为trueValue中的通道值;如果蒙版中的通道值为0x0,结果为falseValue中的通道值。下图演示了选择器蒙版应用于2个Float32x4对象,并生成结果Float32x4对象:

一般编程的时候,分支基于Float32x4的通道值执行true路径和false路径。然后通过执行选择操作合并结果。

5.6 蒙版

一些算法在计算更新通道值的时候,是针对部分而不是全部的通道。虽然你不能操作Float32x4的小部分通道,但是你可以创建一个自定义的选择器蒙版。然后,你可以通过蒙版,使用原始值来合并更新通道。例如:

// v = [2.0, 3.0, 4.0, 5.0]
Float32x4 v = new Float32x4(2.0, 3.0, 4.0, 5.0);

// mask = [0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x0]
Int32x4 mask = new Int32x4.bool(true, true, true, false);

// r = [4.0, 9.0, 16.0, 25.0].
Float32x4 r = v * v;

// v = [4.0, 9.0, 16.0, 5.0].
v = mask.select(r, v);

6、示例

本节包含了一些利用SIMD编写的算法。

6.1 平均值

该例子是计算存储在Float32x4List中的各个浮点数的平均值。循环针对x,y,z和w通道值计算和。循环外,求每个通道和的总和。

double computeAverage(Float32x4List list) {
  Float32x4 sum = new Float32x4.zero();
  for (int i = 0; i < list.length; i++) {
    sum += list[i];
  }
  // 执行一次水平操作
  double average = sum.x + sum.y + sum.z + sum.w;
  return average / (list.length*4);
}

6.2 4×4矩阵乘法

该例子是A和B两个4×4矩阵相乘,结果存入R。

// R = A * B;
void multiplyMatrices(Float32x4List A, Float32x4List B, Float32x4List R) {
    var a0 = A[0];
    var a1 = A[1];
    var a2 = A[2];
    var a3 = A[3];

    var b0 = B[0];
    R[0] = b0.shuffle(Float32x4.XXXX) * a0 + b0.shuffle(Float32x4.YYYY) * a1 + b0.shuffle(Float32x4.ZZZZ) * a2 + b0.shuffle(Float32x4.WWWW) * a3;
    var b1 = B[1];
    R[1] = b1.shuffle(Float32x4.XXXX) * a0 + b1.shuffle(Float32x4.YYYY) * a1 + b1.shuffle(Float32x4.ZZZZ) * a2 + b1.shuffle(Float32x4.WWWW) * a3;
    var b2 = B[2];
    R[2] = b2.shuffle(Float32x4.XXXX) * a0 + b2.shuffle(Float32x4.YYYY) * a1 + b2.shuffle(Float32x4.ZZZZ) * a2 + b2.shuffle(Float32x4.WWWW) * a3;
    var b3 = B[3];
    R[3] = b3.shuffle(Float32x4.XXXX) * a0 + b3.shuffle(Float32x4.YYYY) * a1 + b3.shuffle(Float32x4.ZZZZ) * a2 + b3.shuffle(Float32x4.WWWW) * a3;
}

6.3 最大数值

该例子确定了Float32x4List中最大的浮点数。首先,循环确定每个通道最大的数。然后,循环外确定通道最大数中的最大数。

double findLargestNumber(Float32x4List list) {
  Float32x4 largest = list[0];
  for (int i = 1; i < list.length; i++) {
    largest = largest.max(list[i]);
  }
  // 执行一次水平操作
  double x = largest.x;
  double y = largest.y;
  double z = largest.z;
  double w = largest.w;
  double t0 = Math.max(x, y);
  double t1 = Math.max(z, w);
  return Math.max(t0, t1);
}