以下通过剖析一些经验来了解视频解码优化
1 在嵌入式系统中实现MPEG4的视频解码
有两种方法可行
(1)采用ffmpeg(mplayer 的核心就是采用ffmpeg),然后对ffmpeg mp4解码优化
1)对IDCT汇编化,并优化VLD的实现 ->inline&汇编化
2)根据ARM9 cache & cache line的大小做MB的分组,使得每次可以同时处理多个MB
即 对多个MB在一个循环内做VLD--->IDCT-->MC--....... ->耦合
3)优化关键代码段的内存访问(MC) ->inline&汇编化
4)不要使用FFmpeg内置的img_convert()做yuv2rgb转换 ->inline&汇编化
5)对解码库做ARM指令集优化 ->体系结构优化
configured ffmpeg with cpu = ARMV4L would give you a better performance
If you have IPP,you can enable it, you can obtain huge enhancement
IPP=Intel? Integrated Performance Primitives intel高性能构件库 (only for xScale)
(2)用xvid来做,ffmpeg包含的解码库太多,如果你只做MPPEG-4解码,何必用这么复杂的库?
btw,在嵌入式系统中最好用0.9.2版的xvid。
因为1.1.0的版本包含了很多AS的特性,通常在嵌入式系统中都不需要用,并且也不容易实现。
要自己做编码算法的话,不能总想依赖别人,最好还是需要自己花
功夫去实现和优化。因此我觉得从实际出发的话,XVID0.9.2版的比1.1.0的好。
实际上,通常在主频400MHz的平台上,要优化XVID的算法达到CIF实时解码也还是很容易的,最多就一个多月
2 视频解码流程对解码带来的影响
视频解码优化一般 代码量大,而且源代码往往是从其他地方获取得到的,所以阅读比较困难,更别说优化了,最近在优化realVideo,有几点心得:
1)阅读代码前必须先熟悉流程,抓住关键的点,比如视频解码不外乎熵解码,反量化,反变换,插值,重建,滤波,参考帧插入等。
把握住这几个点,可以将代码很快分离出来。
2)分析解码流程,了解解码需要的最小buffer是多大,各个buffer 的位宽多大。
3)根据已经知道的流程,跟踪代码buffer流向,是否存在多余的内存拷贝。想办法将buffer减少,经验说明,减少buffer带来的速度上的提升远大于局部算法的优化。 ->耦合
4)观察程序结构顺序是否合理,不合理的程序结构会导致buffer增大。
这两天研究视频解码顺序,发现先插值后做反变换要比先做反变换再做插值效率要高许多,原因是插值后的位宽是8bits,而往往反变换后是9bits,所以在重建之前要保存插值后的值要比保存反变换后的值要省一半的空间,这样在重建时访问的内存就少很多了。据我了解,大部分高效率的解码器都是先插值再反变换,而且变换后马上做重建,这样既减少内存使用,也避免内存访问抖动太厉害,最终减少缓存不命中。 ->修改解码流程 耦合
3 cache机制对解码带来的影响
先看 http://www.hongen.com/pc/diy/know/mantan/cache0.htm
写透(直写式)和写回(回写式)有着截然不同的操作,在不同的场合,不同的内存块使用不同的回写策略(如果你的系统可以实现的话)要比使用一种策略要高效得多。具体一点,对于反复存取的内存块置成写回,而把一次写入而很长时间以后再使用的内存置为写透,可以大大提高cache的效率。
第一点很容易理解,第二点就需要琢磨一下了,由于写透的操作是,当缓存有该地址的数据时同时更新缓存和主存,当缓存没有该地址数据直接写主存,忽略缓存。当该地址的数据很长时间后才被使用到,那么在使用的时候该数据肯定不在cache中(被替换了),所以不如直接写入主存来得直接;
相反,如果使用写回操作,当cache中有该地址数据,需要更新该数据,设置dirty位,很长时间后再使用该数据或被替换的时候才将其刷进主存,这有占了茅坑不拉屎的嫌疑;而当cache没有该地址数据时,情况更糟糕,首先需要将相应的主存数据(一个cache line)导入cache,再更新数据,设置dirty位,再等待被刷回内存,这种情况不仅占用了cache的空间,还多一次从主存中导入数据的过程,同样占据总线,开销很大。至于为什么要先从主存中导入数据,是因为cache往主存回写数据时是按照一个cache line 单位来写的,但被更新的数据可能没有一个cache line这么多,所以为了保证数据一致性,必须先把数据导入cache,更新后再刷回来。
对于很多视频解码来说,帧写入过程是一个一次性的动作,只有在下一次作为参考帧时才会被使用到,所以帧缓冲内存可以设置为写透操作,而下一次使用它的时候很可能是作为参考帧来使用,而作为参考帧不需要反复的存取,只需一次读操作就可以了,所以效率并不会因为不经过cache而降低。实验证明该方法可以使mpeg4 sp解码提高20-30%的效率。
相似的内容cache操作的小技巧还有prefetch操作,prefetch操作是将主存的数据导入cache而期间cpu不需要等待,继续下一条指令的执行,如果下一条指令也是总线的操作,那么就必须等待prefetch完成以后再开始。所以,在使用该指令时,在prefetch指令后面插入尽可能大于一次缓存不命中所需要的clock数对应的指令,那么prefetch与其后面的指令可以并行执行,从而省去了等待的过程,相当于抵消缓存不命中的损失。当然,如果插入的指令太多而cache太小,有可能prefetch的数据进入cache后又被替换掉了,所以,这需要自己去评估。 ->cache优化
4 总结
IDCT是视频解码中关键步骤中的第一步,目前一般采用快速算法来做,如chen-wang 算法,c语言和汇编的效果差别还是比较大的。
对一个8x8的block做idct做变换,如
for (i = 0; i < 8; i++)
idct_row (block + 8 * i);
for (i = 0; i < 8; i++)
idct_col (block + i);
把他汇编后,主要是可以减少存储器带宽,提高存储效率,避免无谓的内存读写。
mplayer在此方面做了很多努力,针对armv4(s3c2440属于armv4l架构)的相关文件放在dsputil_arm_s.S文件中。但遗憾的是,它里面有一条指令PLD,cache预取指令2440是不支持的。PLD指令属于enhanced DSP指令,在armv4E(E 既代表enhanced DSP)才被支持,因此在我们orchid上跑的代码必须注释掉这条指令,否则编译不过
再把话题转回来,在IDCT之前,视频压缩流通过VLD(variable lenght decode)变长解码得到DCT数据。
这部分工作一般是通过查表来加速性能,所有的编码表会预先存起来。而取视频比特流的代码通常是宏,
通过宏的扩展来达到和汇编同样的效果。
在IDCT后还有关键的运动补偿和色彩空间转换两个步骤。对运动补偿的加速也是通过汇编化,其代码也同样放在
dsputil_arm_s.S 有必要一提的是在这部分,如果有SIMD指令将会极大的提高它的速度。
color space转换是解码输出后的最重要的一步。在嵌入式系统中,一般都是采用rgb565既16bit来表示一个像素的色彩。
一个8x8的block,它的yuv(420格式)表示如下,
YYYYYYYY
YYYYYYYY
YYYYYYYY
YYYYYYYY
UUUUUUUU
VVVVVVVV
注意它的值是8bit的,通过装换方程计算,可以得到像素值。在实现中通常采用查表来加速计算,对于每一个Y,U,V都有
一个对应表。对于1个320x240的video,共76800像素。如果每个像素在这个转换中节省10个cycle,那save下来的cpu还是相当可观的。
当色彩空间转换完后,就是通过把这个picture copy到framebuffer的内存里,这里存在一大片的copy时间。有两方面可以注意,
一是有人实现过把转换后的内存直接往framebuffer送,减少最后所需的copy过程,这个idea确实不错,但是需要一些技巧去实现
二是copy这个过程本省也是可以加速的,在armv5以上的体系结构里,cpu----cache---memory,其中cache和memory的宽度是32位,
但cpu和cache的bus width确是64位,用32位的成本实现了64位的存储器。如果这个能被使用,那么理论上,copy速度可以加倍。
在PC机上,一般我们的应用程序会有fastmemorycopy这个函数,它们是用simd等特殊指令来实现,在armv5上则是通过它的总线宽度来加速
在s3c2440上不可用:( 它是v4架构。
总的来说,
(1)算法级的优化基本用无可用,ffmpeg/mplayer已经实现的相当不错,除非自己实现一个新的decoder;
(2)在代码级,主要是通过关键代码的inline(宏,inline函数)和汇编来加速。这部分在arm平台还是有一些潜力可挖
(3)硬件级,在这一层,cpu的体系结构决定指令集、cache的形式和大小等。如指令集是否有enhanced DSP指令、SIMD指令
,cache是否可配置、cache line大小,这些都会影响代码级和算法级的优化
(4)系统层优化,之所以把它放在最后一层,是由于它建立在整个系统之上,只有对整个系统包括硬件和软件有深刻的理解才能做到。
纵观优化,其实质是尽可能的去除冗余计算,最大化的利用系统硬件资源。
对于RISC架构的cpu来讲,先天不足的就是需要比较大的存储器带宽(因为RISC的指令都是基于寄存器的,必须把操作数都load到内存才能计算),
cpu资源被过多的使用在内存的read和write。
以以下代码为例,它是解码输出后,把yuv空间装换成rgb空间的一个片断
000111c :
111c: e92d4ff0 stmdb sp!, {r4, r5, r6, r7, r8, r9, sl, fp, lr}
1120: e1a0a000 mov sl, r0
1124: e5900038 ldr r0, [r0, #56]
1128: e1a0c001 mov ip, r1
112c: e3500004 cmp r0, #4 ; 0x4
1130: e24dd034 sub sp, sp, #52 ; 0x34
1134: e1a00002 mov r0, r2
1138: e1a01003 mov r1, r3
113c: 0a00055d beq 157c
1140: e59d2058 ldr r2, [sp, #88]
1144: e3520000 cmp r2, #0 ; 0x0
1148: d1a00002 movle r0, r2
114c: da00055b ble 1574
1150: e59d3060 ldr r3, [sp, #96]
1154: e58d1030 str r1, [sp, #48]
1158: e5933000 ldr r3, [r3]
115c: e59f2434 ldr r2, [pc, #1076] ; 1598 <.text+0x1598>
1160: e0213193 mla r1, r3, r1, r3
1164: e58d3018 str r3, [sp, #24]
1168: e59d305c ldr r3, [sp, #92]
116c: e58d1000 str r1, [sp]
1170: e5933000 ldr r3, [r3]
1174: e79a1002 ldr r1, [sl, r2]
1178: e58d301c str r3, [sp, #28]
117c: e5903008 ldr r3, [r0, #8]
1158: e5933000 ldr r3, [r3]
115c: e59f2434 ldr r2, [pc, #1076] ; 1598 <.text+0x1598>
1160: e0213193 mla r1, r3, r1, r3
1164: e58d3018 str r3, [sp, #24]
1168: e59d305c ldr r3, [sp, #92]
116c: e58d1000 str r1, [sp]
1170: e5933000 ldr r3, [r3]
1174: e79a1002 ldr r1, [sl, r2]
1178: e58d301c str r3, [sp, #28]
117c: e5903008 ldr r3, [r0, #8]
1180: e59c4008 ldr r4, [ip, #8]
1184: e590e000 ldr lr, [r0]
1188: e59c2000 ldr r2, [ip]
118c: e5900004 ldr r0, [r0, #4]
1190: e59cc004 ldr ip, [ip, #4]
1194: e58d3014 str r3, [sp, #20]
1198: e1a011c1 mov r1, r1, asr #3 ;h_size
119c: e3a03000 mov r3, #0 ; 0x0
11a0: e58d4010 str r4, [sp, #16]
11a4: e58d2004 str r2, [sp, #4]
11a8: e58de020 str lr, [sp, #32]
11ac: e58d000c str r0, [sp, #12]
11b0: e58dc008 str ip, [sp, #8]
11b4: e58d1028 str r1, [sp, #40]
11b8: e58d3024 str r3, [sp, #36]
11bc: e1a08003 mov r8, r3
.................................................
.................................................
我们可以发现在这个片断中有太多的ldr(load, read from memory)和str(store, wirte to memory)
而且过多的load和str还影响了cpu和memory之间的cache的效率,形成cache抖动。当发生cache miss时,
cahce控制器花了大力气把内容从memory搬到cache,但是没怎么用这个entry马上又被替换掉。如果运气不好,
cache就一直这样"抖动"。
在解码过程中,各个模块都各自为战,都各自去占比较大的memory带宽
如何减少这种无用的行为呢?必须让关键代码适应硬件体系结构,把数据流相关的代码耦合在一起。
很多代码通过模块化得到了优秀的可读性和可扩展性。鱼与熊掌不可兼得,耦合在一起的代码会显得比较晦涩难懂。
ffmpeg/mplayer在这方面作了一个比较好的tradeoff。
1、2、3的知识摘自网上,要比较好的理解以上内容需要一些视频编、解码的知识。