笔记来源《计算机体系结构 量化研究方法》第四章向量体系结构
接着上一篇继续写:向量体系结构:向量执行时间-CSDN博客
这一节解决上一讲最后留下众多问题中的两个问题:
(1)面对向量长度与处理器向量寄存器长度不匹配的情况(如VMIPS中寄存器长度为64),如何设计高效的策略来兼容和优化这类程序执行?
(2)分析如何有效处理含有条件分支(IF语句)的代码,使之能够向量化,从而扩展向量化的应用范围?
解决方法就是使用向量长度寄存器(1)和 向量遮罩寄存器(2)
向量长度寄存器
向量处理器就像一台高级搅拌机,擅长一次性处理很多水果(数据元素)。这台搅拌机的“杯子”(向量寄存器)能装下固定数量的水果,比如VMIPS搅拌机能装下64块水果。但是,不是每次我们都有正好64块水果需要处理,有时候多,有时候少。
为了适应不同数量的水果(数据),设计了一个“计数器”——向量长度寄存器(VLR)。这个计数器告诉搅拌机这次应该处理多少块水果,哪怕这个数量小于或等于搅拌机的最大容量。这样,即便水果的数量变化,搅拌机也能灵活应对。
为什么需要向量长度寄存器
在编写程序时,我们经常不知道确切要处理的数据量(比如循环次数n),它可能根据用户输入或其他因素变化。所以向量长度寄存器确保无论数据多少,都能有效地利用向量处理器的优势。
条带挖掘(strip mining)
如果要处理的水果数量远远超过搅拌机的最大容量(最大向量长度),怎么办呢?这时,“条带挖掘”技术就派上用场了。想象一下,把这些水果分成一个个小堆,每堆不超过搅拌机的最大容量。先处理完一堆,再处理下一堆,直到全部完成。
编程时,意味着我们会创建两个嵌套的循环。外层循环负责遍历这些“小堆”,而内层循环则针对每一堆中的水果进行处理。这样,即使原始数据长度不确定或超过最大向量长度,也能确保高效利用向量处理器。
MVL:最大向量长度
// 初始化变量
int i, j, low = 0; // i 和 j 是循环变量,low 用于追踪当前处理向量的起始位置
int VL = n % MVL; // 计算首段向量的长度,n是总向量长度,MVL是最长向量处理能力
// 使用求模运算找出不足一个MVL长度的部分
// 外层循环,处理完整的MVL长度的向量段
for (j = 0; j < (n / MVL); j++) { // j 从0开始,每次循环处理一个MVL长度的段,直到处理完所有整段
// 内层循环,处理当前段中的元素
for (i = low; i < (low + VL); i++) { // i 从当前段的起始位置low开始,到本段结束
Y[i] = a + X[i] + Y[i]; // 执行DAXPY操作,Y[i] = a + X[i] + Y[i],这里似乎有个小错误,应该是 Y[i] = a*X[i] + Y[i]
}
// 更新low,准备处理下一向量段
low = low + MVL; // 将low移至下一个MVL长度段的起始位置
// 如果还有剩余数据需要处理,并且剩余数据量大于MVL,则更新VL为MVL,继续处理完整段
if (n - low > MVL) {
VL = MVL; // 下一阶段处理完整的MVL长度
}
}
上面这段类似一个简单的算法题,一次最多可以处理MVL个元素,一共n个元素。
向量遮罩寄存器
问题背景
在高性能计算中,向量化是提高代码执行速度的重要手段,但传统的条件语句(如IF)会成为向量化的一大障碍。因为条件语句会导致数据流的分支,使得编译器难以生成连续、并行的向量指令。Amdahl定律指出,程序中未向量化部分会限制整体加速效果,因此处理好条件执行对提升性能至关重要。
向量化好比是同时给多个人发信,比起一封封单独写和发送,一次性写好所有信然后一起寄出显然效率更高。但是if...else
,在代码中用于根据不同的条件执行不同的操作,在向量化上下文中却成了挑战。想象一下,如果一个循环里的操作(比如加法或乘法)是否执行取决于某个条件,那么数据处理就不能简单地批量进行了。就好比是发信时,你需要先检查每一封信是否符合条件(比如地址是否正确),才能决定是否投递,这就打断了原本流畅的批量处理流程,导致并行计算的优势无法充分发挥。
Amdahl定律
Amdahl定律是一个关于系统加速比的理论,它告诉我们,当对系统的一部分进行加速时(比如通过向量化提升某部分代码的执行速度),系统的整体加速效果受限于那些没有加速的部分。换句话说,即使你将程序的90%优化得飞快,但如果剩下的10%仍然是瓶颈,整体性能提升也是有限的。所以在高性能计算中,解决像条件语句这样的非向量化障碍就显得尤为重要,它们可能成为拖慢整个程序的“短板”。
向量遮罩处理器工作原理
为了解决这一问题,现代向量处理器引入了向量遮罩寄存器(Vector Mask Register)。这个寄存器与向量寄存器类似,但它存储的不是数据,而是一系列布尔值(通常是0或1),每个值对应向量寄存器中一个元素的执行条件。
条件判断:首先,通过向量化的比较指令(如上面示例中的SNEVS.D V1,F0
,比较向量V1中的元素是否不等于F0中的标量值,if(vi==fo))生成一个遮罩向量,遮罩寄存器中每个位置的值代表对应的向量元素是否满足执行特定操作的条件(1表示满足,0表示不满足)。
条件执行:随后,执行向量运算指令时,处理器会参考遮罩寄存器。只有遮罩寄存器中对应位置为1的元素才会参与运算,为0的元素则被“屏蔽”,其在向量中的值不变。
这种机制允许编译器将原本含有条件分支的循环转化为向量操作,即便循环体内部逻辑不同,也能通过遮罩实现并行处理,减少控制流分支带来的性能损失。虽然存在一定的开销(即使某些元素被屏蔽,对应的指令周期仍会被消耗),但相比逐元素的标量处理,整体性能通常有显著提升。