一直在看这方面的东西,自己也写过代码,但是始终都没有掌握其中的真谛。其实现在也是半懂不懂,然而还是感觉有些灵感了,写起代码也不那么慢了。今天下午就调试了四个代码,前提是这四个函数前几天是看过的:dct4x4dc,idct4x4dc,quant4x4dc和iquant4x4dc。效果dct变换还是很好的,基本提高了一半的效率,而iquant就没甚么改进,而且quant还不如以前了,原因估计是代码太长,以后还要进一步优化。
今天调的这么快,主要是把2x2的都做好了,那么简单将进行四个循环就可以了,而最后把循环展开,反正c64是有那么多寄存器的,一下就做完这16个,效果当然要比循环四次好的多(而且从水平和垂直两个方面就是八次循环了)。
今天想写下的心得不是对于dct变换本身的,虽然这部分也是很关键的,但今天就说一下优化dct2x2dc的过程。
void
dct2x2dc_c(int16_t* data)
{
int16_t s[4];
s[0] = data[0];
s[1] = data[1];
s[2] = data[2];
s[3] = data[3];
data[0] = s[0] + s[2] + s[1] + s[3];
data[1] = s[0] + s[2] - s[1] - s[3];
data[2] = s[0] - s[2] + s[1] - s[3];
data[3] = s[0] - s[2] - s[1] + s[3];
}
这是T264中的dct2x2dc的c代码,其实很简单,dct变换就是矩阵变换,最适合做优化了。单独测试这部分代码的c函数,两个主要参考:总周期2157,data cache misses50次。
简单的改为汇编,只要取数做运算就可以了。
.global _dct2x2dc_sa
_dct2x2dc_sa:.cproc data
.no_mdep
.reg data0,data1,data2,data3,data02,data13,data20,data31
.reg s0,s1,s2,s3
LDH *data,s0
LDH *+data[1],s1
LDH *+data[2],s2
LDH *+data[3],s3;分四次把data取出
ADD s0,s2,data02
ADD s1,s3,data13
ADD data02,data13,data0
SUB data02,data13,data1
SUB s0,s2,data20
SUB s1,s3,data31
ADD data20,data31,data2
SUB data20,data31,data3;把公式综合起来进行运算
STH data0,*data
STH data1,*+data[1]
STH data2,*+data[2]
STH data3,*+data[3];存数
.endproc
这样的实现很简单,相关的注释在后面,剖析结果:周期2109,data cache misses46次。
改进非常小。这是个中间阶段,测试数据是正确的,就可以进行进一步优化了。很明显,汇编中最费时的就是load和store。以前就看到数据打包的概念,这次终于理解了。首先,我们知道(主函数中)data的数据是16bit的,而c64的寄存器都是32位,那么我们就可以把每两个数放在一个寄存器中,然后再利用其他相关的操作来进行运算(很明显c64这样的设计就是要我们这样用的)。说起来很不明确,具体的讲就很清楚了。首先,一个LDW在一个地址取一个字,那么就是32bit,就是取了两个data:LDW *data,data10。同时再来一个:LDW *+data[1],data32。很明显就取出了所有的四个data,data0和data1分别位于寄存器data10的低16位和高16位,同样data2和data3在data32中。如果画图就很明白了,这个文字说明:
data10: data1(H)data0(L)
data32: data3(H)data2(L)
而且我们知道c64有两条指令:ADD2和SUB2(具体可以参看相关资料),分别是将两个操作数的高16位相加、相减放在dst的高16位,同时低16位相加相减放在低16位。也就是说,如果我们使用这样两条语句:ADD2 data10,data32,data0213和SUB2 data10,data32,data2031,那么在data0213和data2031中存储为:
data0213: 1+3(H)0+2(H)
data2131: 1-3(H)0-2(H)相当整齐。
之后我们再看c代码,得到的data0到data3正是data0213和data2031高低共四个元素1+3、0+2、1-3、0-2的组合。当然首先要把他们分离,还没发现c64有指令可以高低位计算的,把这两个寄存器右移16得到了1和3的两个运算,原始的(data0213和data2031)就是0和2的运算。这样的计算好像是不对的,因为使用的0、2运算的高16位并不是单纯的,而使用的1、3运算的高16也是与计算没关系的。这个不用担心,我们的结果就是16位,最后只要考虑低16位就可以了,而且我们最后还有pack的操作,就会把高16位冲掉了。我们计算一个data2作为说明。
观察c代码,data2是(0-2)+(1-3),先把0-2移出来:SHR data2031,16,data31;然后再相加即可:ADD data2013,data31,data2。搞定。其他的也是类似了。关于PACK2的运算可以看整个代码就明确了。
LDW *data,data10
LDW *+data[1],data32;整字取数据,这里注意不要因为是按字取就加2,同样LDDW是也是加1
ADD2 data10,data32,data0213
SUB2 data10,data32,data2031;得到四个元素
SHR data0213,16,data13
SHR data2031,16,data31;分离1和3的运算
ADD data0213,data13,data0
ADD data2031,data31,data2 ;计算data0和data2
SUB data0213,data13,data1
SUB data2031,data31,data3 ;计算data1和data3
PACK2 data1,data0,data10
PACK2 data3,data2,data32;打包一个是冲掉高16位,一个也是为STW做准备
STW data10,*data
STW data32,*+data[1];按字再存数据,也是提升性能的主要部分。
为简明起见,去掉了线性汇编的头尾以及定义寄存器。最后的剖析结果:周期1955,data cache misses42次。再进一步想,如果使用双字取数和存,是不是更会提升呢?
LDDW *data,data32:data10
;中间代码与LDW一致
STDW data32:data10,*data最后的剖析结果:周期1877,data cache misses40次。
2x2的情况还看不那么明显,按照这样的方法改了dct4x4dc_c,二者的剖析结果为:
c:4591108
sa:240552(都是上面一样的指标)。
可见,提升了近一半的性能。
其他函数的优化也是相类似的。
目前的理解就这么多了,其他的更深刻慢慢就会知道了。