|
data access patters/数据访问模式(关键词:函数之间,函数内的数据访问模式对性能的影响)
由于FPGA的优越性能,FPGA被选中来实现C代码。一个FPGA的大规模并行架构允许它执行的操作比一个CPU处理器的固有的顺序操作的速度更快。用户通常都希望利用这一性能。
这里的重点理解是理解C代码中固有的访问模式对结果可能产生的影响。虽然最受关注的是那些进入和推出硬件功能的访问模式,但在硬件功能中的任何瓶颈都会对函数的访问速率产生负面影响,因此考虑函数内的访问模式也是值得考虑的。
为了突出强调某些数据访问模式如何对性能产生负面影响,并演示如何使用其他模式来完全支持FPGA的并行性和高性能功能,本节将通过一种图像卷积算法来阐述
1.algorithm with poor data accesee patters/数据访问模式差的算法
这里使用一个标准的卷积函数来演示C代码是如何对FPGA的性能产生负面影响的。这个例子中,对数据执行一个水平的,然后是垂直的卷积,由于图像边缘位于卷积窗口之外,最后一部是处理边界周围的数据。
算法的结构可以概括为:
1.水平卷积
2垂直卷积
3.对边界像素操作
水平和垂直卷积都是用一个固定长度的模板进行卷积,这样会导致边界像素不能够通过卷积模板后得到结果,优化手册中是直接使用最靠近边界区域的有效填充
。
//代码理解见P22
static void convolution_orig(int width,int height ,const T *src,T *dst,const T *hcoeff,const T *vcoeff)
{
T local [MAX_IMG_ROWS*MAX_IMG_COLS];//图像面积
//水平卷积
HconvH:for(int col=0;col<height;col++)//从图像的第0行到最后一行
{
HconvW:for(int row=border_width;row<width-border_width;row++)从图像的卷积滤波中心到(列操作)
{Hconv:for(int i =-border_width;i<=border_width;i++)
{
}
}
//垂直卷积
HconvH:for(int col=border_width;col<height-border_width;col++)
{
HconvW:for(int row=0;row<width;row++)
{
Hconv:for(int i =-border_width;i<=border_width;i++)
{
}
}
//对边界元素操作
}
这样的C代码直观易懂,但是C代码中的一些问题会对硬件结果的质量产生负面结果。问题具体表现在:
(1)编译需要大量的存储:
第一个问题是C编译过程中需要大量的存储,算法的中间结果存错在一个内部本地数组中。这需要一个高度*宽度的数组,比如说对于1920*1080的图像,它将容纳2073600个值。
对于针对ZYNQ和ultrascale MPSOC以及许多主机的的交叉编译器,这个本地存储的数据量将会导致在运行的时候堆栈溢出。
当一个函数只能够运行在硬件上,为了避免这种问题的一个有用的方法是使用_synthesis——宏,当硬件函数被集成到硬件时,系统编译器自动定义这个宏。上面的代码在C仿真过程中使用动态内存分配来避免编译问题。并且只在合成过程中使用静态存储。值得一提的是:
使用这个宏的缺点是C仿真验证的代码与实际合成的代码不一样。但是,在这种情况下,代码并不复杂,行为也将相同。
上面讨论硬件函数使用动态内存分配来避免需要大量的存储,但是在硬件喊函数上动态分配内存,消耗的将是BRAM,这对于FPGA设备将会有很大的要求。块RAM的使用可以通过使用数据流优化和数据流通过小型高效的FIFO最小化。但这将需要的数据被用于一个流的顺序,而目前还没有这样的要求。
(2)本地数组初始化:
这个问题与性能的表现有关,循环clear_local用于将阵列局部值重新设置为0.即使这个循环在硬件中以高性能的方式执行,这个操作任然会需要大约两百万个时钟周期(将存储一幅图像的数组都清零)。并且当内存初始化的时候,系统不能够执行任何图像处理(循环中是串行的)。解决思路是在次内存初始化时,使用在内循环Hconv的一个零时变量在写入数据数据前初始化。
最后,可以得出的一些结论是:
1)数据的吞吐量,系统的性能,从根本上受到数据访问模式的限制。
2)高效能的FPGA关键之一是最小化PS端的读写。
3)以前获取的每个数据访问都会对系统的性能产生负面影响。
4)FPGA能够执行许多并行计算并达到非常高的性能,但不能通过重新读取值来中断数据流。
5)为了最大限度提高性能,数据应只从PS端中访问一次,并且本地存储最小的数据单元,这些数据也应该考虑到复用。(不是很懂)
2.algorithm with optimal data access patterns
上面主要讨论实现卷积的关键是以最少的资源进行高性能设计,具体方式包括:
1)通过系统最大化数据流:避免使用任何阻止数据连续传输的编码技术和算法行为。
2)最大化数据的重用:使用本地缓存确保没有重新从外面读取数据的要求,并且传入的数据可以保持连续的流。
3)接受条件分支:这对于CPU和GPU是昂贵的开销,但是对于FPGA是最优的
为了使得算法具有最优的访问模式,我们首先讨论数据是如何通过系统在FPGA上流入和流出。
卷积算法是在图像上执行的,当来自图像的数据生成或被销毁时,它以标准的光栅扫描方式传输。图见P29
如果数据以流方式传输到FPGA上,FPGA应该以流的方式处理它,并且以这种方式将其从FPGA中返回。(这些以流方式访问的数据,最终实现中被优化为单寄存器)
(1)优化的行卷积:
这个算法必须使用K个以前的样本来计算卷积结果,因此将样本复制到零时缓存hwin中。使用本地存储意味着不需要从PS端重新读取值并中断数据流。其中,对于第一次计算,由于没有没有足够的值在hwin中去计算结果,故没有输出值被写入。
该算法一直读取输入的样本并将其保存刀hwin中。每次梅毒一个新的样本,它将不需要样本值移除hwin。在第K个输入数据已读后,可以将第一个输出结果写出(满足了第一次卷积条件,完成第一次卷积)。算法根据这种方式将数据沿着行进行读取,直到最后的样本被读取完毕。只有最后的样本存储在hwin中,就能够计算完卷积所需要的全部内容。
在整个过程中,SRC输入的样本都是以光栅流的方式处理,每个样本依次读取,任务的输出要么被使用,要么被丢弃。但是任务的不断进行计算,这与CPU中执行的代码不同。
(2)行和列卷积优化:
(3)边界卷积优化:
3.optimal data access patterns/数据访问类型优化
下面概述了如何确保数据访问模式在FPGA上达到最佳性能:
1)最小化输入数据读取:在数据读入BRAM后,它可以很容故意满足多并行路径,但是对硬件函数的输入可能是性能的瓶颈。因此,读取数据一次,如果数据需要重用,请使用本地缓存。
2)尽量减少多数组的访问,尤其是大数组:数组在BRAM中实现,像I/O口只有有限数量的端口,这可能会导致性格的瓶颈。因此可以将大数组划分成更小的数组,甚至单个寄存器,但将大数组分区将导致使用许多寄存器,建议使用小的本地化缓存来保存结果,如累积。然后将最终结果写入数组。
3)寻求在流水线任务中执行条件分支(而不是条件执行任务,甚至流水线任务)。条件语句在管道不同的路径实现(条件语句是顺序执行的),允许来自一个任务的数据流入下一个任务,在下一个任务中执行将导致更高性能的系统。(增加并行度?)
4)将写出最小化:这与读取的原因相同,即I/O端口有限导致系统性能的瓶颈。复制额外的访问只会加深系统的问题。 |
|