SIMD优化

向量化计算

向量化计算是一种特殊的并行计算的方式,相比于一般程序在同一时间只执行一个操作的方式,它可以在同一时间执行多次操作,通常是对不同的数据执行同样的一个或一批指令,或者说把指令应用于一个数组/向量。

eigen

SIMD

Single instruction, multiple data 单指令多数据流

依赖于CPU的向量寄存器,一次指令对多个数据进行相同的操作。

  • 数据内存中连续储存
  • 对于连续数据的操作相同或者规律相同
  • 处理的数据间没有相互依赖关系
  • 寄存器带宽比数据类型大。。数据对齐

Intel-SSEARM-NEON

SIMD代码改造优化的tips:

循环优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 连续内存数据循环在最内层
for (int i = 0; i < H; i++)
for (int j = 0; j < W; j++)
a[i,j]++;

// 考虑cache的循环拆分
for (int i = 0; i < H; i++)
for (int j = 0; j < W; j++)
a[i] = a[i] + b[j];
for (int j = 0; j < W; j += batch)
for (int i = 0; i < H; i++)
for (int jj = j; jj < j + batch; jj++)
a[i] = a[i] + b[jj]
...

移除分支

关于分支预测失败的性能损耗可以参考这里(尽管分支预测成功率很高)

等效的数学公式替代分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    for (int c = 0; c < arraySize; c++)
if (data[c] >= 128)
sum += data[c];
// equivalent
for (int c = 0; c < arraySize; c++) {
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
}
// -O2 if被优化成cmovge -> ADD sum_t data[c]
ADD sum_t -128
cmovge sum sum_t

// cmp swap
if (v1 > v2) { new_v = v1; new_i = i1; }
else { new_v = v2; new_i = i2; }
// simd version
new_i = vbslq_f32(vcgtq_f32(v1, v2), i1, i2); // vcgt 比较a>b则对应位置全置1,否则0; vbsl 如果对应位置1则取a,否则b;
// 类似于 bool ge = a > b; int max = ge * a + !ge * b;
new_v = vmaxq_f32(v1, v2);

数据重排

必要的时候对数据进行重新排列,以配合SIMD达到性能提升。

输出blob的reshape、对齐

因为SIMD的特性,除了需要保证数据对齐128字节,而且要求数据内存连续;所以需要根据具体的算法流程确定好数据的内存排布,如网络输出应该是NCHW还是NHWC还是要NC4HW4对齐,在实现前需要确定清楚。

数据处理前对读取的数据在寄存器内部重排

有时候在需要处理相邻的数据时,使用SIMD可能达不到好的效果,这时候可以考虑将数据进行转置,比如AA-sort中进行in-core排序前对4x4的数据进行了转置,以便于后续的比较,见这里;其实就是reshape的衍生。

消除数据存取延迟与数据依赖延迟

由于CPU指令流水线等机制,消除指令间的依赖关系能够提高指令的执行效率

通过数据预取减少内存读写延迟

数据预取能加快数据读取速度

通过指令横向扩展抵消内存读写延迟

ld v1 → dosth v1 → st v1 指令可能因为内存读写阻塞

横向扩展 ld v1, v2 → dosth v1 ,v2→ st v1 ,v2 像这样一次处理两组,则内存阻塞等待时间相当于减少一半

通过指令横向扩展消除数据依赖延迟

v2 = dosth v1 → v3 = dosth v2

横向扩展 v2, v22 = dosth v1, v11→ v3, v33 = dosth v2, v22

可以消除指令间的依赖关系,提高执行效率

NEON

ARM cpu拓展结构,包括64-bit和32个128-bit的寄存器,以提供SIMD方式高效的图片视频数据处理能力。提供汇编代码和内联函数调用的使用方式。

寄存器结构:Elements are the standard Neon-supported widths of 8 (B), 16 (H), 32 (S), or 64 (D) bits.

数据存取,除了提供数据顺序存取,还提供数据交叉存取,方便处理多通道的数据。

其中内联函数仅提供从低位0开始填充寄存器的读取方式,而汇编可以指定寄存器偏移。如使用方式为:LD3 { V0.B, V1.B, V2.B }[4] , [x0], #48

其他资料

官方文档整理了NEON数据重排可以用到的一些指令:https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/neon-programmers-guide-for-armv8-a/coding-for-neon/permutation-neon-instructions

其他的内联函数指令查询:https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/intrinsics?search=vtrn

网友提供的内联指令中文介绍: https://github.com/rogerou/Arm-neon-intrinsics](https://github.com/rogerou/Arm-neon-intrinsics

开发指南: https://developer.arm.com/documentation/den0018/a (离线版本:DEN0018A_neon_programmers_guide.pdf

ARM、neon汇编指令中文介绍:RealView汇编指南中文版.pdf