欢迎光临散文网 会员登陆 & 注册

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

2023-10-31 20:05 作者:先进编译实验室  | 我要投稿

向量程序优化

本节介绍在已经改写的向量化程序上如何更进一步的提升性能,主要包含以下六点内容:

  1. 不对齐访存;

  2. 不连续访存;

  3. 向量重用;

  4. 向量运算融合;

  5. 循环完全展开;

  6. 全局不变量合并;

不对齐访存

访存对齐性是影响向量程序性能的重要因素,内存对齐访问是指内存地址A 对n求余等于0,其中n为访存数据的字节数。如果向量访存是不对齐的,与对齐的向量访存相比,需要额外的开销才能实现数据的存储操作。在编写向量程序时,应尽量使用对齐的访存指令。然而现实程序中更多的是不对齐访存,优化人员可以借助程序变换的方法,将不对齐访存调整为对齐以提升向量程序的性能。例如使用循环剥离方法可以将循环迭代中非对齐的部分从循环中剥离出来,使主体循环变为内存访问对齐的循环。编译器默认从A[0]开始进行对齐访问,而主体循环从A[1]开始访问,因此如果不进行循环剥离,在进行向量化时对数组A和B的访存是不对齐的。利用循环剥离将循环的前3次迭代剥离出去,从循环的第4次迭代开始转为向量执行,那么主体循环中的向量操作均可以转为对齐内存访问。

源代码:

当多维数组的最低维长度不是向量长度的整数倍时,难以判断访存的对齐性,此时一般会使用非对齐访问指令保证程序的正确性。这个问题可以使用数组填充来解决,即当数组最低维长度不是向量化因子的整数倍时,通过增加数组最低维的长度,使得向量化的时候能够统一按照对齐的方法进行向量化装载或者存储。

源代码:

数组填充后:

现实程序中数据访存不对齐的情况更多,如果程序实际是不对齐访存,而写为对齐访存,那么就会造成程序运行错误。因此,如果优化人员不能确定访存的对齐性时,需要使用不对齐指令进行访存,以保证程序的正确运行。

源代码:

不对齐指令改写:

不连续访存

连续的向量访存不仅可以提高向量访存指令的效率,还可以提高向量寄存器中有效数据的比率。但多数计算都不是理想的连续访存情况,本节将介绍如何在不改变引用顺序和数据布局的情况下,利用处理器提供的向量指令实现不连续访存程序的向量化。

源代码:

不连续访存改写后的代码:

然而并不是所有的平台都支持聚集和分散指令,对于一些不连续访存、但是访问内存有规律的程序可以利用向量重组实现不连续访存,向量重组是指当目标向量中的所有元素不在同一个向量中时,通过多个向量之间的重新组合得到目标向量。

对于上一节不对齐访问的优化示例,并不是所有的平台都支持不对齐的访存指令,当平台不支持不对齐访存指令时,可以借助于数据重组实现不对齐访存。如通过两次对齐访存,然后将数据移位或者重组。

使用Intel处理器的SIMD扩展指令集,进一步如何说明利用向量重组实现不连续访存程序的向量化。可以通过混洗指令simd_vshff将数据重组。假设VA={a0,a1,a2,a3};VB={b0,b1,b2,b3}表示复数取出向量化数据,a0、a2为实部,a1、a3为虚部,VB同样如此,当掩码为(2,0,2,0)时,可以得到VC={a0,a2,b0,b2},当掩码为(3,1,3,1)时,可以得到VC={a1,a3,b1,b3},VC中前两位数据只能来源于VA,后两位数据只能来源于VB。

源代码:

混洗指令改写后的代码:

混洗指令不仅可以用作数据整理,当掩码为零的时候也可以用作数据广播填充。如下:

通过以上使用混洗指令,实现了float类型的数据转换成四个槽位的相同数据,作用类似于向量取值指令。在编写程序时,可以根据实际情况使用不同的方式进行数据填充转换。

向量重用

不对齐访存代码可以利用向量重用进一步优化。以下面代码为例,在使用对齐指令对C赋值时,循环体内对于不对齐数组的向量访存需要两条对齐的向量访存和一条拼接指令,可以将其中一次访存指令重用。下面代码中用到了向量的局部重用。在拼成V1向量时使用了va1寄存器中的后2个值,以及va2中的前2个值,在下一次迭代中需要使用到本次迭代中va2中的后两个值,因此对于下次循环迭代来说,直接将本次va2中的值赋于下次迭代计算的va1可省去一次访存操作。

源代码:

使用对齐指令改写后的代码:

向量寄存器的重用可以减少或者去除访存的需求。向量寄存器含有多个数据项,因此向量寄存器的重用包括全部数据项的重用和部分数据项的重用,向量寄存器的完全重用是最理想的情况,将避免后续所有的向量访存,如下:

源代码:

使用向量指令改写后的代码:

向量重用后:

向量运算融合

向量运算融合就是将多条向量运算指令合并为一条向量运算指令,以提高向量程序的执行性能。可以将加法指令和乘法指令融合为向量乘加指令,一般情况下乘加指令和乘法的节拍数一致,假设乘法指令的指令周期为6,加法指令的指令周期为4,那么向量运算合并后提升了(6+4)/6=1.67倍。不是所有的向量运算指令都可以合并,它需要复合向量运算指令的支持,常见的复合向量运算指令包括向量乘加、向量负乘加、向量乘减、向量负乘减等。

源代码:

使用向量指令改写后的代码:

乘加指令改写后的代码:

循环完全展开

循环展开不仅可以提高程序的指令级并行还可以提高寄存器重用,优化人员可以在循环被向量化后继续对循环进行展开,相当于在发掘完程序数据级并行的基础上,进一步发掘程序的指令级并行,同时提升向量寄存器的重用。

假设使用的向量寄存器长度为128位,一次能够处理4个float数据,向量化后原来的循环仅需要两次迭代,因此优化人员在展开时可以将循环完全展开,去掉循环控制结构,如下:

源代码:

循环完全展开:

全局不变量合并

循环不变量是指该变量的值在循环内不发生变化。向量化的过程中会引入很多向量类型的循环不变量,如果未将向量类型的循环不变量移到循环外,将影响程序的向量化性能。循环内含有循环不变量C,向量化的过程中需要将C转为向量类型,这个过程一般利用向量设置指令。

源代码:

不变量外提后的代码:

如果其它循环在向量化过程中也产生了同样的向量常数,可以在过程内甚至程序内进行更大范围的常数合并。

向量改写后的代码:

不变量外提的代码:


自编教材分享:第七章—数据级并行(三)的评论 (共 条)

分享到微博请遵守国家法律