加速的关键性考虑因素
当决定在算法中采用并行措施时,务必要留意架构上固有的一些特性:
总线位宽:数据怎样在CPU和其他部件之间传递,对整体效率和吞吐量都有着重大的影响,因为总线决定了数据可被多快地传递,输入与输出都会受到其影响。位宽太窄或速度太慢,都会成为一个处理上的瓶颈,从而影响扩展指令的执行效率,但如果总线不能被及时地填充数据以充分利用,那么不管扩展指令多有效率,都是无用的。
一般而言,总线的频率决定了扩展指令发出的频密度,而位宽则决定了扩展指令一次能处理多少数据。注意,某些架构提供了CPU与各部件之间的多重总线,由此来进行硬件加速,例如Stretch公司的S5以软件配置的处理器,在应用处理器与可编程部件之间,就有三个读取与两个写入端口,每个均为128位的位宽,并提供每条扩展指令最大读取384位及写入256位的能力。有效地利用这些总线,开发出高效的并行机制并不是件难事。
在带宽计算上,要保证带宽可把结果传回至主CPU并同时处理中间结果,这一点尤其重要。
状态寄存器:为一个复杂算法加速,通常会把算法进行分割,以便用于加速的硬件在每一次迭代中只计算结果的某一部分。因为转换器是作为一个部分函数执行,也就是说,总体结果中只有一些离散的段是通过扩展指令执行的,而且,经常会有一些中间结果需要传递给下一个函数的调用,例如,在FIR函数最初的C实现中,一个连续的总数或中间结果必须在主算法循环的每一次迭代中都保留。再者,当应用更先进的优化技术时,中间结果的数量也会逐步增加。
为达到加速的目的,必须在每次迭代之间防止产生这些中间结果,因为这些值经常会通过数据总线传回到主CPU,而CPU又会把它们返回给函数,以便进行下一次迭代。此过程不仅消耗有限的总线带宽,而且也浪费了宝贵的CPU时钟周期,降低了整体处理的效率。
为保证效率,在此可使用状态寄存器。状态寄存器提供了一种机制,其可存储函数迭代的中间结果,而又不会涉及到主CPU或在总线上传递数据,因此,利用状态寄存器,可使整体的并行机制变得更有效率,而可用的状态寄存器数目,又进一步决定了在算法中可多大程度地利用这类并行机制。 数据大小:许多的架构在有关某数据类型占用的位宽方面,只提供了一个有限的子集,例如,一个布尔变量通常只需要一比特位,但在实现中,大小经常至少是一个字节,而一般对CPU来说,这样做会提高效率,因为对一个比特位进行掩码操作所花的代价及性能上的影响,远远要比浪费7个比特位更多;当传递数据给扩展指令,而此时CPU又必须立即剥离和打包这些比特位时,就会损失一部分效率。另外,为节约可编程逻辑资源,寄存器必须能被定义以消除未使用的位。最后,如果未使用的位可在被传递到总线之前就消除,那么总线带宽还是可以充分利用的。理想中的情况是,这些问题应在可编程逻辑之内处理,因为来自CPU所需的操作降低了整体的速率,而此时,CPU通常是可发出扩展指令的。
并行计算 对FIR转换器硬件实现进行优化的第一步,是决定总线可高效地传给加速硬件的数据点数量。通过一个高速的宽带总线,可同时传输多个数值,例如,一个执行8次乘法和加法(MAC)的扩展指令,将以8为系数减少内部循环迭代的次数,每个流水线扩展指令可在单一周期内高效地执行,因此,整体性能将提高大约8倍。请注意,如果某一特殊的数据集不能被8整除,那么数据集将会被填充零直至为8的倍数,因此,就不一定需要特殊的末端循环指令了。
数据相乘的方法如图2所示,以高亮表示的数据点区域代表了单一扩展指令执行的运算,在扩展指令每次执行时,它都通过适当的系数乘以8个数据,并把乘积累加起来。产生的部分和将存储在局部状态变量中,随后的指令便把这些部分和相加,由此便可消除把部分和传回给CPU,随后再传给扩展指令这个过程。
图2:数据相乘的方法 |
每条扩展指令执行8次MAC,可把执行范例FIR转换器所需的理论最小周期数降低到1941,与直接用C实现的方法相比,性能上可提高15倍。
可同时运算的最优数量,依赖于可用的可编程资源数量,因为这些资源是在CPU当前访问的各种扩展指令之间共享的,所以,合理地节约使用可编程资源,对最大化提高系统整体性能来说,就显得极其重要,只要可能,就要尽量复用这些资源。
复用的效率可通过使用两条扩展指令来作一演示,分别是FIR_MUL和FIR_MAC(见例2)。
例2:
#include <stretch.h> static se_sint<32> acc;
/* 执行8路并行MAC */ SE_FUNC void firFunc(SE_INST FIR_MUL, SE_INST FIR_MAC, WR X, WR H, WR *Y) { se_sint<16>x, h se_sint<32> sum ; int i ; sum = 0; for(i = 0; i < 128; i += 16) { h = H(i + 15, i); x = X(127-i, 112-i); sum += x * h ; } acc = FIR_MUL ? sum : se_sint<32>(sum + acc) ; *Y = acc >> 14 ; } |
在FIR转换器第一次调用时,部分和状态需要重设,这通常由FIR_MUL处理。在64路转换器的情况中,将有7个并发的迭代调用FIR_MAC,它们都将使用先前已有的部分和。FIR_MUL与FIR_MAC的主要不同之处在于下面这一行代码:
acc = FIR_MUL ? sum : se_int<32>(sum + acc) |
如果扩展指令通过FIR_MUL被调用,上述代码将有效地重设部分和,如果通过FIR_MAC调用,将直接使用部分和。
这种实现的好处就是复用了分配给剩余函数的可编程逻辑,同一可编程资源能被共享,就不需要为了这两条扩展指令,而增加可用资源了。合理地使用这些资源,并有选择性地解决与之相关的延迟,在这种情况下,就不会对性能产生实质性的影响,因而,就有更多的资源可用于实现其他扩展指令或进一步优化当前指令。