循序渐进做优化:从C62x到C64x一例(中)
三、应用C64x系列指令的优化程序
首先,在应用C64x系列指令集进行优化时,在寄存器使用上也有很大的优势。因为C62x系列的两个通用寄存器组分别有16个寄存器,并且其中的A10~A15、B10~B15没有自动保存功能,在使用之前要进行压栈处理,函数功能执行完毕后再从堆栈中取出数据。而64x系列的两个通用寄存器组分别增加了16个可以自动保存的寄存器,这对于程序设计来说有很大的方便,可以通过寄存器的大量使用来换取程序运行指令周期的减少。甚至,因为寄存器足够多常常避免了寄存器多赋值(multiple assignment)问题,减少了要求关闭中断的时机。
同时,在交叉通道的使用上,C64x的.D功能单元也与交叉通道相连,同时C64x允许一侧的多个功能单元由交叉通道从另一侧读同一个源。基于C64x指令集设计的优化函数主要有以下几个演进过程:
1、应用扩展及打包类指令进行优化
对于本文的这个函数,考虑到两个字节做加法时的溢出问题,将字节型数据扩展为半字型数据进行运算,并利用C64x系列指令集中的一些指令,每个指令周期对两个半字同时进行处理。又由于源操作数是字节对齐的,因此需要使用ldndw(无边界调整双字读取指令)一次读取8个字节,既可以保证字节对齐,又可以减少读取次数;根据函数的实际应用情况来看,由于一次内循环的8个数据能够保证字对齐,因此在存储之前再将结果打包成字节型,使得可以使用stw指令在一个周期内存储8个结果数据。优化程序的内核如下:
hloop: ;循环内核
add .d1x hC23,hB34,hC23
|| add .d2x hA45,hB56,hB56
|| unpkhu4 .l1 hA4567,hA67
|| unpkhu4 .l2 hB5678,hB78
|| unpklu4 .s1 hA0123,hA01
|| unpklu4 .s2 hC1234,hC12 ;1
sub .d1 hcnt,1,hcnt
|| add .s2 hB56,hconstb,hB56
|| add .l2x hA67,hB78,hB78
|| add .l1x hA01,hC12,hA01
|| add .s1 hC23,hconsta,hC23
|| ldndw .d2t2 *hB_src++[hsrcbstride],hB5678:hC1234 ;2
ldndw .d1t1 *hA_src++[hsrcastride],hA4567:hA0123
|| sub .l1 hC23,hrounding,hC23
|| sub .l2 hB56,hroundingb,hB56
|| add .d2 hB78,hconstb,hB78
|| add .s1 hA01,hconsta,hA01
|| [hcnt]B .s2 hloop ;3
sub .l1 hA01,hrounding,hA01
|| sub .l2 hB78,hroundingb,hB78
|| shr2 .s1 hC23,1,hC23
|| shr2 .s2 hB56,1,hB56 ;4
shr2 .s1 hA01,1,hA01
|| shr2 .s2 hB78,1,hB78 ;5
spacku4 .s1 hC23,hA01,hA01
|| spacku4 .s2 hB78,hB56,hB78 ;6
stw .d1t1 hA01,*hdst++[hdstastride]
|| stw .d2t2 hB78,*hdstb++[hdstbstride] ;7
unpkhu4 .l1 hA0123,hC23
|| unpklu4 .s1 hA4567,hA45
|| unpkhu4 .l2 hC1234,hB34
|| unpklu4 .s2 hB5678,hB56 ;8
在上面的汇编优化程序中,使用了扩展和打包类指令将源操作数在字节型数据和半字型数据之间转换,再加上对半字型数据进行操作的移位指令shr2,可以看到.S功能单元的使用非常紧张。上面的优化程序与C6205的优化程序比较起来内核长度减少到8个指令周期,内核循环的执行次数仍为7次,整个函数为72个指令周期,节省了16个指令周期。
2、使用求SIMD指令进行优化处理
从上面基于C64x系列指令集的优化程序不禁想到,如果能够使用字节运算代替半字运算,整个函数的执行周期数将有很可观的改善,但是主要困难在于加法的溢出问题。从函数的运算表达式来看,很容易想到C64x系列指令集中对无符号字节型数据求平均值的avgu4这条指令,该指令是在一个指令周期内对两个源操作数的四个字节分别对应求平均值并且无需考虑溢出。对单个字节的执行表达式可表示为:
dst=(src1+src2+1)>>1
而C语言函数的表达式与该指令的表达式只差在变量rounding上。从实际应用中我们知道,rounding的值为“0”或“1”,因此考虑将函数的表达式改为:
dst=[(src1-rounding)+src2+1]>>1
此时需要对其中的一个源操作数进行减一操作,由于该函数可用于mpeg-4标准中的inte-block解码中水平插值的计算,源操作数很可能为零,这样没有饱和保护会使结果产生向下溢出(underflow)。对于C64x系列指令集来说,没有办法在很短周期内做出饱和减法的功能,因此考虑能否用加法的形式实现。
从rounding的取值来看,如果rounding=0,则与avgu4的执行表达式相同,可以直接使用;如果rounding=1时,则可将函数表达式改为:
dst=[(src1-rounding+2)+src2+1]>>1-1
即原函数的表达式可以改为:
if(rounding=1) dst=(src1+1+src2+1)>>1-1;
else dst=(src1+src2+1)>>1;
程序中可表达为:
if(rounding=1) src1=(src1+1);
dst=(src1+src2+1)>>1;
if(rounding=1) dst=dst-1;
这种改写方式确保了在rounding取不同值时都可以使用avgu4这条指令,并可以保证执行的正确性(关于加法带来的溢出可能,下一节有讨论)。
由于avgu4这条指令有一个周期的指令延迟,又使用.M功能单元,从而使用该指令后与原来需要做加法、移位运算的方法相比节省了其他功能单元的使用。为了能够更好的运用流水线机制和充分利用C64x系列的通用寄存器组,采用连续读取两组数据的方法进行处理。综合上述因素考虑后的汇编优化内核程序为:
hloop: ;循环内核
ldndw .d2t2 *hB_src++[hsrcbstride],hB5678:hB1234
|| [hroundingb]add4 .l2 hB5678,cst1b,hB5678
|| [hcnt]bdec .s2 hloop,hcnt
|| [!hroundinga] mv .s1x hB1234,hA1234
|| [hroundinga]add4 .l1x hB1234,cst1a,hA1234 ;1
ldndw .d1t1 *hA_src++[hsrcastride],hA4567:hA0123
|| avgu4 .m1 hA1234,hA0123,hA0123
|| avgu4 .m2x hB5678,hA4567,hB4567
|| [!hroundinga] mv .s1x hD1234,hC1234 ;2
|| [hroundinga]add4 .l1x hD1234,cst1a,hC1234
|| [hroundingb]add4 .l2 hD5678,cst1b,hD5678
ldndw .d2t2 *hB_src++[hsrcbstride],hD5678:hD1234
|| avgu4 .m1 hC1234,hC0123,hC0123
|| avgu4 .m2x hD5678,hC4567,hD4567 ;3
ldndw .d1t1 *hA_src++[hsrcastride],hC4567:hC0123
|| [hroundinga]sub4 .l1 hA0123,cst1a,hA0123
|| [hroundingb]sub4 .l2 hB4567,cst1b,hB4567 ;4
stw .d1t1 hA0123,*hdst++[hdstastride]
|| stw .d2t2 hB4567,*hdstb++[hdstbstride]
|| [hroundinga]sub4 .l1 hC0123,cst1a,hC0123
|| [hroundingb]sub4 .l2 hD4567,cst1b,hD4567 ;5
stw .d1t1 hC0123,*hdst++[hdstastride]
|| stw .d2t2 hD4567,*hdstb++[hdstbstride] ;6
可以看到内核缩短为6个指令周期,同时内核的执行次数仅为3次,由此可见,函数的执行效率可以大大提高。
作者:张廷廷 钱浙滨 更新日期:2005-06-04
来源:embeddedcore.com
浏览次数:
相关文章
相关评论 发表评论
- No Comments