一、编译反馈及优化
TIC6000 的编译工具可以对代码进行各种优化,以提高代码的执行速度,并减小代码尺寸。这些优化包括简化循环,软件流水,语句和表达式的顺序重排和分配变量到寄存器等。
(1)-O3:表示使用最高级别的优化,编译器将尽可能使用各种优化技术,例如软件流水,循环优化,循环展开,对函数声明进行重排等。
(2)-ms:当打算减小代码尺寸时,不用降低优化的级别,可以用-ms 选项来控制代码尺寸。当设置了-O2或-O3时,编译器主要是优化性能,这时使用代码尺寸标志 -msn 使编译器更加注重代码尺寸,使用不同级别n的 -ms 选项可以在代码性能和尺寸之间做权衡。高 -O 的级别和高的 -ms 级别一起使用可以得到最小的代码尺寸。
(3)-pm:程序级优化,它把所有的源文件编到一个中间文件中进行优化,这个中间文件称为模块,编译器对这个模块进行优化和生成代码。由于编译器能“看到"整个程序,所以它进行的优化是文件级优化所达不到的,例如如果一个函数中的一个特殊的变量的值总是一样的,编译器会用这个值直接代替该变量,直接进行值传递而不是参数传递。
(4)mt:表明没用使用别名来访问变量,它告知编译器,它可以假设代码中指向对象的指针都是唯一的,编译器能更加积极地进行优化。
(5)-mv6400+:开启针对64x+CPU结构进行的优化。
二、循环优化
一般来说,程序的运算主要耗费在循环的执行上,因此如何提高循环的性能对整个程序的速度有着决定意义。软件流水基于C6000的并行架构,是编排循环指令,使循环的多次迭代并行执行的技术。软件流水通常只对最内层循环进行。如果内层循环太短,循环次数太少,则软件流水线还没充满就已经进入排空阶段,不能充分利用CPU的并行处理特性。在EHMM 算法的第二步即观察概率的计算中(EstimateObsPro函数),共有5层循环,运算量非常巨大,其中最内层的循环是对子状态数计数的,EHMM 的子状态数只有3 或6 两种情况,属于非常低的循环次数;而次外层循环是对每行的观察值进行计数的,对于一幅 100×100 的图片,如果扫描窗的步长为2,则每行可以产生约 50 个观察值,循环计数达到50,大大高于子状态数。因此我们改写了这一部分循环的顺序,使最内层循环对每行观察值计数,结果使运算速度提高了约7%。
循环体内的判断语句、跳转语句和非正常终止语句(break)会严重破坏软件流水,例如在EstimateObsPro 函数中,存在着对混合项个数m的判断,由于我们把所有子状态的GMM设成具有相同的混合项个数,因此能够把这一判断移除。最后提及的是,对于最内层循环用到的动态内存,OpenCV的开源代码是在每次进入循环的时候分配的,我们把这一操作移到了循环体外,使一次分配的内存在整个循环过程中都能使用,上述两项修改进一步降低了CPU周期。
我们知道,每个循环在结束前都会迭代多次,迭代的次数就是循环计数器,编译器利用循环计数来确定一个循环是否能流水,为了填充流水线,软件流水的循环结构需要循环中的迭代至少执行某个次数,当编译器不能确定循环计数时,会为循环代码生成两个版本,一个是流水版本,一个是不流水版本,编译器通过对循环计数的判断来确定执行哪一版本,但这样一次循环总有一个版本没被执行,这个没有被执行的循环就是冗余循环,通过设置MUST_ITERATE 程序指令可以告诉编译器该循环执行次数的信息,从而关闭冗余循环的生成,并且由于编译器掌控了循环计数的信息,能够更加有效地安排流水,从而提高执行速度。例如在EHMM 的观察概率循环中,使用了如下的程序指令,这条程序指令的各个参数依次告诉编译器,该循环的最少执行次数是3,最大执行次数是6,并且执行次数总是3的倍数。
三、内联优化
内联是 C6000 编译器的一个特色,它是一组具有C调用接口的特殊的函数,这些函数直接映射为C6000 的CPU指令,任何不易用C/C++表达式表示的语句都可以通过内联函数实现。例如定点DSP的加法通常要考虑溢出,即相加的结果超过字长所能表达的最大(小)值。一般加法的结果会回绕,饱和加法的结果是该字长的最大(小)值。用C/C++来表达饱和加如下:
上面复杂的表达式可以用一条内联来代替: _sadd(a,b),它会映射为一条单周期的C6000指令,大大节省运算量。在我们的EHMM 的程序中主要使用了四个内联函数,分别为_mpy32ll(), _smpy(), _sadd(), _ssub(),前两个是C64x+特有的指令,第一个是结果为64位的32位乘法,第二个是结果为32位的定点Q3l乘法,后两个分别是饱和加及饱和减法。使用内联函数后对主要函数的性能提升如下表:
四、内存、CACHE和DMA优化
DM6446的DSP核是一个C64x+的核,内部集成了32K的一级程序缓存,80K的一级数据缓存和64K的二级程序/数据缓存。这两级缓存都既可以作为片内RAM又可作为cache,CPU可以无阻塞地访问一级缓存,而二级缓存会阻塞2~8个周期。如何合理利用这些片上存储器资源对程序性能起着决定性的作用。经过分析后我们把C64x+核的片内空间作如下划分:
L1DRAM 和CPU 是处于相同速率的,但容量有限,由于算法的复杂性,算法使用到的数据不能全部装入到LIDSRAM,因此在计算过程中我们需要逐步把要用到的数据调入到片上内存,用过的数据调出至片上内存。数据在片上片外内存的搬运可以通过DMA来完成,这样数据的搬运就能够与计算并发进行,形成流水。在Adaboost 检测算法中我们使用了这一技术。由于Adaboost 的强分类器是分层的,两层之间没有相关性,因此我们在LIDRAM中开劈了两个缓冲区bufA和bufB进行乒乓操作,这一过程如图7-5所示:
改进前后的性能比较如下表:
利用cache 剖析工具可以分析cache 的命中情况,对cache 利用效率不高的地方,通过代码调整或内存调整以达到提高cache 利用率的目的。通过查看编译后的map文件,占运算量70%以上的EstimateObsProb函数代码量约为3.7KB,因此整个函数都能调入到L1Pcache中,通过cache剖析工具也看到L1Pcache命中率高达99%,因此关键部分是数据cache,需要提高L1Dcache的命中率,降低其缺失率。下面是缺失的类型及其避免方法:
(1)、冲突缺失:是由于一块以上的数据引用,映射到同一个cache块上,并且缺失不是由于cache太小而引起。冲突缺失可以通过改变数据代码在存储器中的相互位置来消除;
(2)、强制缺失:也称为首次引用缺失,数据或程序第一次访问时其内容肯定不会缓存在cache中,因此必然会产生缺失,这种缺失是无法避免的;
(3)、容量缺失:是由于cauche的容量小于引用数据所产生的缺失,消除容量缺失的方法,是将被访问的数据分成一些小块,或将循环加以划分,在数据被排出cache前充分尽可能充分地访问它。
经过cache优化后,L1Dcache的命中率也达到了90%以上,而识别速度则提高了约7%。