浅谈向量化和SIMD
为什么要有向量化
当存在大量数据可供应用程序同时计算时,我们称之为数据并行性。传统的标量指令每次只能处理一个数据,而向量指令(SIMD)则可以将相同的操作应用于多个元素,即单指令多数据。很多应用需要这种细粒度的并行性,比如经典的多媒体应用。
向量化简单示例
以数组求和为例,假设4向量长度的SIMD指令。
float vsum(float *v, int n){
float s = 0; int i;
for (i=0;i<n;i++)
s += v[i];
return s;
}
下面把c改写成如下形式,模拟SIMD过程。
float vsum(float *v,int n){
float vs[4] = {0.0,0.0,0.0,0.0,};
float s = 0.0;
int i;
for(i=0;i<n-4;i+=4){
vs[0] += v[i];
vs[1] += v[i+1];
vs[2] += v[i+2];
vs[3] += v[i+3];
}
s = vs[0] + vs[1] + v[2] +vs[3];
for(;i<n;i++){
s += v[i];
}
return s;
}
在向量化后,每次操作会进行4个数据的分别累加,最后会进行全部的累加。当数组并非4的倍数时,如11,15,则还需考虑末尾的边角处理,对剩下的数据进行单独标量累加。
SIMD的缺陷
- SIMD 架构的共同特征是将多个数据元素打包到一个固定宽度的寄存器中。由于寄存器大小是固定的,如果不添加新指令和寄存器,就无法将 ISA 扩展到新的硬件并行级别,造成的后果就是为了保持向后兼容,新推出的处理器除了实现新的拓展指令集之外,还需要兼容之前所有的旧的指令集。使得完整的指令集不断膨胀,自1978年以来,IA-32指令集已从80条增加到大约1400条,主要是由SIMD推动的。
- 当循环中要处理的数组元素数量不是SIMD寄存器中元素数量的倍数时,需要在软件中实现特殊的循环尾部处理。例如,如果一个数组包含 99 个 32 位元素,并且 SIMD 体系结构为 128 位宽(即一个 SIMD 寄存器包含四个 32 位元素),则可以在主 SIMD 循环中处理 4*24=96 个元素,并且 99 -96=3 个元素需要在主循环后处理。常见的场景是必须使用标量(非 SIMD)指令来实现尾部的累加,但如果标量和 SIMD 指令具有不同的功能或语义,那很容易会引发问题,通常在循环之前你还需要额外的控制逻辑。例如,如果数组长度小于 SIMD 寄存器宽度,则应跳过主 SIMD 循环。添加的控制逻辑和尾部处理代码损害了代码密度(再次降低了指令缓存效率),并增加了额外的开销。MIPS-32 MSA和IA-32 AVX2的代码的三分之二至四分之三的代码开销用于为主SIMD循环准备数据或在处理边缘元素。
解决上述所有缺陷的一种替代方法就是向量寄存器。向量架构是一种较旧的,更优雅的利用数据级并行性的替代方法。向量计算机从主存储器中收集对象,并将其放入顺序的长向量寄存器中。
RISC-V向量扩展简介
RISC-V向量扩展是RISC-V指令集的标准扩展模块之一(简称RV-V),它主要为基础指令集添加了向量寄存器和各类向量指令,使得用户可以使用向量化来优化和加速程序代码。相比于传统的SIMD指令,RV-V实际上是一个变长的向量指令扩展,它有两个重要的CSR寄存器,即vtype和vl,vtype寄存器主要用于说明向量元素的类型。vl寄存器用于保存实际执行的向量长度。vtype和vl寄存器都是通过使用vsetvli指令进行更新设置。
由于向量长度可以动态定义和调整,在向量化时就可以把主循环和边角处理合在一起。如:当对len=7的数组向量化时,向量长度为4,则在第一轮中vl将会被设为4,由于剩下3个三个小于向量长度,则第二轮中可把vl设为3,进而对剩下元素向量化。
此外,为了有效地处理长向量,RV-V提供了一种寄存器组合机制,使得在不改变向量长度的情况下,利用更多的向量寄存器,支持处理更宽的元素。参数LMUL表示组合寄存器数量,同时引入了一个新的参数:VLMAX= LMUL * VLEN / SEW,即融合后的可执行的最长向量长度。(VLEN:一个向量寄存器的宽度,SEW:实际元素的宽度)。如:当对len=1024的数组向量化时,可以使用指令vsetvli t0, a0,e8,m8
,其中m8
是将LMUL设置为8,即将8个向量寄存器分组形成长向量寄存器以提高处理效率。在vlen=128,sew=32时,根据计算可得到VLMAX=32,即每轮vl都会被设为32。