自编教材分享:第七章—数据级并行(一)



数据级并行是指处理器能够同时处理多条数据的并行方式,大部分处理器采用SIMD向量扩展作为计算加速部件,SIMD扩展部件可以将原来需要多次装载的标量数据一次性装载到向量寄存器中,通过一条向量指令实现对向量寄存器中数据元素的并行处理。使用SIMD方法执行的代码称为向量代码,将标量代码转换成向量代码的过程即为向量化。通常使用两种方式获得向量程序:
(1)由程序设计人员编写向量代码。
(2)借助于编译器的向量化编译自动生成向量代码。
为了让程序获得更好的向量化性能,优化人员需了解程序在向量化过程中面临的问题,这样不仅有利于辅助编译器生成更有效的向量程序,也有利于自行编写出高效的向量程序,将从向量程序的编写和优化两个方面展开具体介绍。示例多以intel SIMD扩展指令改写,为了便于说明,本文使用128位向量寄存器操作指令进行演示。常用的指令按照释义划分如下表所示。

向量化的本质是重写程序,以便其同时对多个数据进行相同的操作。编写向量化程序时可以从循环、基本块以及函数等层次发掘数据的并行性,本节将分为以下六个部分展开讨论。
(1)循环的向量化
(2)基本块的向量化
(3)函数的向量化
(4)分支的向量化
(5)归约的向量化
(6)合适的向量长度
循环的向量化
当需要计算的数据较多时,直接进行计算需要多个for循环,代码冗长且不好理解。而将循环向量化后可以将多次for循环变成一次计算,较为方便且代价小。有些循环不适合直接进行向量化,此时可以使用循环变换技术对循环进行变换,例如循环分布可以将可向量化和不可向量化的语句分块,循环剥离可以使得循环内的数据引用变得对齐,循环交换可以使得内层循环的访存变得连续,同时还可以通过将某个外层循环交换至最内层进行向量化。
下方代码中,每次迭代内赋值由一次变为四次,循环的迭代次数由原来100次减少为25次,在理想情况下该程序向量化后运行速度可以提升近4倍。
原始代码:
128位向量指令改写后:
面对多层循环时,最内层循环的依赖关系更容易计算清楚,因此一般都选择最内层循环作为向量化的目标。以矩阵乘代码为例。
原始代码:
128位向量指令改写后:
当最内层循环存在依赖环向量化后不再满足正确性要求或优化效果不佳时,且无法使用循环交换的前提下,可以考虑对外层循环进行向量化。
示例代码在Intel平台上进行测试,只对内层循环向量化的代码相对于原始代码加速比为1.12,而进行外层循环向量化的代码相对于原始代码加速比为1.9。由此可以看出,不同的向量化方案性能存在明显的差异,选择一个合适的SIMD向量化方案是取得好的向量化优化效果的关键。
外层循环向量化后的代码为:
基本块的向量化
面向基本块的向量化又叫直线型向量化,其要求基本块内有足够的并行性,否则会因为有大量的向量和标量之间的转换而影响向量化效果,基本块级向量化方法与循环级向量化方法不同,是指从指令级并行中挖掘数据级并行。
面向基本块的向量化方法中常常提到打包、解包的概念,包是一个同构语句的集合,即语句参数可能不同但可编译的部分是相同的,将多条同构语句组成包的过程称为打包,反之则称为拆包。
同构语句:
考虑面向基本块的向量化时,代码中非同构语句更为常见,但非同构语句会影响后续向量化,此时可以将代码中的非同构语句转为同构语句。
示例一:
非同构语句:
转换为同构语句:
示例二:
非同构语句:
转换为同构语句:
示例三:
代码中S1,S2两条语句都对数组C进行写操作,但是等号右侧的运算不是完全同构的,此时可以提取两条语句中的公共子表达式。提取之后可以仅对公共子表达式进行向量化,其余部分仍然保持标量执行,但这个过程涉及到标量执行和向量执行之间的转换,因此优化人员应该确认此种做法的收益性。
非同构语句:
转换为同构语句:
//同构
循环展开将迭代间并行转为迭代内并行,可以增加循环内语句的数量,因此优化人员可以在展开后的循环块内实施面向基本块的向量化。在未进行循环展开且向量化因子为4的前提下,只能将基本块内的前4条语句转为向量执行,后2条语句没有向量化成功。此时可以循环展开4次。
示例一:
原始代码:
基本块向量化之后:
示例二:
循环展开4次后,可以对循环体内24条语句实施基本块级向量化,循环展开4次后再采用基本块向量化方法避免了生成不连续访存指令,程序的运行效率得到了提高。
原始代码:
循环展开4次后基本块向量化:
对于原来循环,如果展开4次可能会造成基本块内语句的条数过多,优化人员也可以在展开2次后实施基本块级向量化。展开2次后基本块内含有12条语句,语句数量适中,便于优化人员进行向量打包。具体循环展开次数可以根据程序的特征进行选择,不同的展开次数以及不同的向量化方法会获得不同的向量化效果。
循环展开两次后基本块向量化:
函数的向量化
不论是面向循环还是面向基本块的向量化方法都是在标量函数内的,即该函数的参数为标量。而函数级向量化是将几个相邻的计算实例合并为一个向量实例,是一种单程序多数据的程序,即函数的参数为向量,返回值也为向量。
根据标量函数fun1生成一个向量函数vecfun1,循环内用向量函数vecfun1代替原来的标量函数fun1,假设SIMD扩展部件一次可以处理4个int数据,那么在向量化后的函数调用点处,每4个标量函数(fun1 (x0,y0),fun1(x1,y1), fun1(x2,y2),fun1 (x3,y3))由一个向量函数vecfun1(<x0…x3>,<y0…y3>)代替,函数vecfun1的参数为向量(<x0…x3>,<y0…y3>),返回值为向量<z0…z3>。
原始代码:
函数向量化后: