本文主要介绍了基于DSP平台的音视频编解码算法的C语言优化方法,如常用的内联函数,数据打包SIMD,函数组合,数据的reuse重用,循环loop的优化,针对结构体和宏定义的优化,对条件控制语句的优化,并以ADI Blackfin
DSP处理器上的H.264编码器优化思路。
1、内联函数方法:
使用内联函数替代复杂的C语言程序:即使用直接对应汇编指令的库函数来实现C代码,可以大大优化。
比如
for(i = 0; i < N; i++)
tmp1[i] = tmp2[i];
可以使用以及简单的memcpy(tmp2, tmp1, N*sizeof(type));来替代,可以大大提高运行效率。
内联函数既能够去除函数调用所带来的效率负担又能够保留一般函数的优点。然而,内联函数并不是万能药,在一些情况下,它甚至能够降低程序的性能。因此在使用的时候应该慎重。
1.我们先来看看内联函数给我们带来的好处:从一个用户的角度来看,内联函数看起来和普通函数一样,它可以有参数和返回值,也可以有自己的作用域,然而它却不会引入一般函数调用所带来的负担。另外,它可以比宏更安全更容易调试。
当然有一点应该意识到,inline specifier仅仅是对编译器的建议,编译器有权利忽略这个建议。那么编译器是如何决定函数内联与否呢?一般情况下关键性因素包括函数体的大小,是否有局部对象被声明,函数的复杂性等等。
内联汇编
对时间要求苛刻的部分可以用本地汇编来重写。结果可能是速度上的显著提高。然而,这个方法不能想当然的就去实施,因为它将使得将来的修改非常的困难。维护代码的程序员可能对汇编并不了解。如果想要把软件运行于其他平台也需要重写汇编代码部分。另外,开发和测试汇编代码是一件辛苦的工作,它将花费更长的时间。
2、数据打包处理技术SIMD:
对短字节的数据使用宽长度的存储器访问,如使用word访问两个short类型的数据,即一种数据打包处理技术,有利于寄存器级的优化,可以充分利用总线宽度,减少数据访问次数
3、函数组合:
主要针对调用次数很多的函数,牺牲结构化的体系特性,和模块化设计,将使用频率很高的几个函数组合成一个函数,减少调用开销。
4、减少内存访问操作次数
提供数据的reuse重用性,只有当数据不被使用时才保存输出;
5、全局变量代替动态分配
牺牲了变量的最小权限原则,动态分配存在数据对齐问题,因而在存取跨越不同数据块的内存单元时可能出现冲突,需要延迟指令。
对于复杂的运算语句,可以用查找表的方法来实现
在一个程序中影响性能的主要代码通常是循环,尤其是具有深层嵌套的循环往往占据程序执行时间的大份额。优化一个循环,较好的方法是抽出这个循环,使之成为一个单独文件或者函数,对其进行重新编写、重新编译和单独调试。
循环展开:代码中循环越多,执行的效率越低。
因此,我们考虑采用循环展开的方法,将多循环变为少循环,甚至是单循环。即运用消除冗余循环的方法来提高指令并行执行的程度,从而提高代码的执行效率。充分分解小的循环,循环展开有利于处理器多流水线结构对指令进行规划组合和调度。
Switch 可能转化成多种不同算法的代码。其中最常见的是跳转表和比较链/树。推荐对case的值依照发生的可能性进行排序,把最有可能的放在第一个,当switch用比较链的方式转化时,这样可以提高性能。此外,在case中推荐使用小的连续的整数,因为在这种情况下,所有的编译器都可以把switch 转化成跳转表。
尽可能使用常量(const)
尽可能使用常量(const)。C++ 标准规定,如果一个const声明的对象的地址不被获取,允许编译器不对它分配储存空间。这样可以使代码更有效率,而且可以生成更好的代码。
提升循环的性能
要提升循环的性能,减少多余的常量计算非常有用(比如,不随循环变化的计算)。 进入循环前分支已经确定,就可以减少对分支预测的依赖。 把本地函数声明为静态的(static)
考虑动态内存分配
动态内存分配(C++中的";new";)可能总是为长的基本类型(四字对齐)返回一个已经对齐的指针。但是如果不能保证对齐,使用以下代码来实现四字对齐。这段代码假设指针可以映射到 long 型。
例子
double* p = (double*)new BYTE[sizeof(double) * number_of_doubles+7L];
double* np = (double*)((long(p) + 7L) &; –8L);
现在,你可以使用 np 代替 p 来访问数据。注意:释放储存空间时仍然应该用delete p。
使用显式的并行代码
尽可能把长的有依赖的代码链分解成几个可以在流水线执行单元中并行执行的没有依赖的代码链。因为浮点操作有很长的潜伏期,所以不管它被映射成 x87 或 3DNow! 指令,这都很重要。很多高级语言,包括C++,并不对产生的浮点表达式重新排序,因为那是一个相当复杂的过程。需要注意的是,重排序的代码和原来的代码在代数上一致并不等价于计算结果一致,因为浮点操作缺乏精确度。在一些情况下,这些优化可能导致意料之外的结果。幸运的是,在大部分情况下,最后结果可能只有最不重要的位(即最低位)是错误的。
提出公共子表达式
在某些情况下,C++编译器不能从浮点表达式中提出公共的子表达式,因为这意味着相当于对表达式重新排序。需要特别指出的是,编译器在提取公共子表达式前不能按照代数的等价关系重新安排表达式。这时,程序员要手动地提出公共的子表达式(在VC.net里有一项"全局优化"选项可以完成此工作,但效果就不得而知了)。
推荐的代码
float a, b, c, d, e, f;
...
e = b * c / d;
f = b / d * a;
float a, b, c, d, e, f;
...
const float t(b / d);
e = c * t;
f = a * t;
推荐的代码
float a, b, c, e, f;
...
e = a / c;
f = b / c;
float a, b, c, e, f;
...
const float t(1.0f / c);
e = a * t;
f = b * t;
结构体成员的布局
很多编译器有"使结构体字,双字或四字对齐"的选项。但是,还是需要改善结构体成员的对齐,有些编译器可能分配给结构体成员空间的顺序与他们声明的不同。但是,有些编译器并不提供这些功能,或者效果不好。所以,要在付出最少代价的情况下实现最好的结构体和结构体成员对齐,建议采取这些方法:
按类型长度排序
把结构体的成员按照它们的类型长度排序,声明成员时把长的类型放在短的前面。
把结构体填充成最长类型长度的整倍数
把结构体填充成最长类型长度的整倍数。照这样,如果结构体的第一个成员对齐了,所有整个结构体自然也就对齐了。下面的例子演示了如何对结构体成员进行重新排序
使用宏来代替函数运算。
通用算法实现需要对数据类型进行重新定义,以适用于各种平台间的移植(因为不同的编译环境对数据类型的定义可能不同),如在ARM C编译器中定义的char类型是8位无符号的,而不像一般的编译器默认是8位有符号的。在armcc的命令行的编译选项中可以使用-zc可以把char转换为有符号类型的。
通用算法设计还需要考虑模块化的设计,以便于移植。典型的如音频解码中使用的综合滤波器组,需要首先进行IMDCT变换,然后加窗、重叠相加。
局部变量尽可能的声明为32位的,因为所有的arm寄存器都是32位的。把局部变量从char和short类型转化为int类型,可以改善性能并减少代码的尺寸。同样的对于函数参数也有效果。对于函数参数和返回值应尽量避免使用char和short类型。即使参数范围比较小,也应该使用int类型,以防止编译器做不必要的类型转换。
对于存储在主存储器的数组和全局变量应尽可能的使用小尺寸的数据类型以节省存储空间。
由于隐式或者显式的数据类型转化通常都会有额外的指令周期开销,所以在表达式上尽量避免使用。
对于固定次数的循环,使用减计数循环(decrementing loop)比使用增计数循环(incrementing loop)更好。
循环继续的条件使用i!=0比使用i>0更好(对于i为有符号数而言)。或者都使用无符号的循环计数值。
for(i=64;i!=0;i--)更好。
为了减少循环开销,可以展开循环体,重复循环体多次,并按照同样的比例减少循环次数来降低循环开销。
过程调用标准是4寄存器规则,使用4个或者更少的参数的函数,要比多于4个参数的函数执行效率高的多。若参数较多,可以使用结构体来归纳参数。
结构体的元素按照元素的大小进行排列,以最小的元素放在开始,最大的元素放在最后。
改变程序结构,减少判断分支跳转,可能会增加代码的size:
for(i=offset1;i
优化总结
第一阶段:首先,产生C代码并进行时间评估。一般情况下,这个阶段的代码性能很低。如果经过评估后,仍然满足不了实时要求的话,需要进入第二个阶段以进一步改进代码性能。
第二阶段:利用优化选项、内联函数以及其它优化方法改进C代码。如果代码仍不能达到所期望的效率,则进入第三阶段。
第三阶段:从C代码中抽出对性能影响大的代码段,用线性汇编重新写这段代码,然后利用汇编优化器优化该代码,直到代码满足要求为止。
4.2 优化和评估C代码
代码分析结果显示DCT、IDCT变换、运动估计运算量占程序总运算量的比重很大,因此这部分函数是程序优化的重点。为此,我们通过下述方法对C代码进行了优化:
(1)对于复杂的运算语句,可以用查找表的方法来实现,以节省耗时。比如:在运算表达式中出现了"/"以及"%"等符号时,可以先按照所有可能的输入计算出所有可能的输出,以后的运算就可以省略而只需要查表得到数值。该方法的本质在于用空间换时间。同样,对于if-else关系到数据运算的选择语句,也可根据具体的情况,采用查找表的方法来实现。
(2)对于通过查找表编码部分,可以将相关的码表进行合理的编排,以便运用一条指令可以一次查到需要的数据。同时,尽可能用多位的指令来访问少位的数据。比如:使用int型(32位)访问2个short(16位)型数据,将其分别放在32位寄存器的高16位和低16位字段。这样,可以提高一倍的数据读取效率。同样,像使用int型可一次访问两个short型一样,使用double型访问可一次读2个float型数据(4个int型数据),从而减少对内存的访问次数,从而减少运算耗时。
(3)在C代码中,使用内联函数替代复杂的C代码。内联函数是可直接映射为C6000指令的特殊函数,使用时同调用其它函数一样调用,同时不会破坏系统环境。内联函数用前下划线(如:函数_add2 (int src1,int src2)表示src1,src2的高低半字分别做有符号加法,返回结果)表示,使用内联函数可快速优化C代码。
(4)代码中循环越多,执行的效率越低。 因此,我们考虑采用循环展开的方法,将多循环变为少循环,甚至是单循环。即运用消除冗余循环的方法来提高指令并行执行的程度,从而提高代码的执行效率。
(5)为了进一步提高代码性能,经过评估,找出影响速度的关键 C代码段 (DCT/IDCT变换和运动估计)用线性汇编重新编写。线性汇编是C6000系列DSP所特有的类汇编工具。只需按照C代码的自然顺序,写出线性汇编语句,同时不必考虑功能单元的分配,以及指令的并行性。从而,它比编写纯汇编语句耗时要少,又具有较高的执行效率。如果编写线性汇编仍不能达到指标要求的话,在运用纯汇编编写相关的代码,充分利用C6000
DSP结构以及指令集的特点,尽可能并行其中的非相关语句。从而进一步减少代码的执行时间和提高程序的性能。
4.3 存储器的优化
与PC机相比,DSP的程序数据存储空间非常有限。因此,对于视频编解码这种需要处理大量数据的程序而言,必须合理安排数据和程序的存储方式,实现对存储器的优化,以便提高程序执行的效率。否则,大量数据的反复搬移会阻碍程序运行效率的提高。
采取以下方法对存储器进行优化:分析代码,把被反复调用的程序段(如DCT变换和DCT反变换)放在片内程序存储区中,把频繁用到的数据段(如编码表)放在片内数据存储器中,把不常用到的程序和数据段放在片外存储器中,以避免对程序或数据进行不必要的反复搬移。
在H.264程序运行过程中,由于一帧图像的数据量很大,故而将参考帧数据放到片外,需要用到当前块和参考窗数据时,再将它们从外存搬运到内存中,以便提高效率。
总体优化
总体优化主要包括两部分内容:程序模块化的设计及数据结构的设计。
程序模块化设计时,既要考虑模块的独立性,又要考虑模块的完整性。笔者的H.264的模块关系图如图3所示。视频输入模块负责图像序列的读取,从PPI口进入的图像首先保存到外部存储器,再进行编码。读入的图像经帧间模式选择和帧内模式选择模块,得到每个宏块的预测模式。整系数变换、量化模块对预测后的残差进行整系数变换及量化处理。量化后的系数经过扫描后,在编码模块中进行UVLC编码。最后由写码流模块输出。其中,去块效应滤波模块对反量化、整系数逆变换后的重建图像进行滤波;图像缓存管理模块负责管理对参考图像的存取。从图3中可以看出,整系数变换与量化结合在一起作为一个模块。主要因为:一方面H.264中,量化和整系数变换本身就部分结合在一起;另一方面这样可以在寄存器中一起完成变换、量化,有助于减少数据的存储次数和读取时间。对于反量化、整系数逆变换,采用相似的设计思路。
数据结构的设计是H.264编码的重要组成部分。合理的数据结构既有利于提高数据访问的速度又有利于程序的不同平台的移植。主要有以下原则:尽可能连续存放数据,这样有利于用DMA方式对数据进行读取;每种数据结构完成相对简单的功能,这样便于对不同数据结构的数据进行管理。如表示帧间模式的InterMode、帧内模式的IntraMode需要放在片内存储器中以加快读取速度,而参考帧的数据ImgData由于数据太大则需要放在片外存储器中;尽可能使用短的数据类型,节省DSP的存储空间。
2.2 各程序模块的优化
各程序模块的优化主要指各模块的C代码优化及部分代码的汇编优化。
C代码的优化对H.264编码器有着重要意义,它既有利于提高编码的速度,又有利于编码器的跨平台移植。C代码优化有下面的原则:
(1) 使编码器代码线性化,这样有利于DSP的流水线满负荷运行,更充分地发挥DSP的数据处理能力。
(2) 取消循环中的数据依赖。数据依赖是指后面指令的输入数据依赖于前面指令的输出数据。许多DSP芯片都提供了硬件循环指令,Blackfin533有两个硬件循环器,可提供两层的硬件循环。硬件循环实现了零开销的循环判断,能大大提高循环指令的执行速度,然而数据依赖的存在会阻止硬件循环的使用。所以要尽可能消除循环中的数据依赖。
(3) 将除法转化为乘法或查表方式。Blackfin533提供了硬件乘法器,但没有硬件除法器。执行除法指令会花费几十或上百个指令周期。将除法转化为乘法或查表,能大大减少这种开销。
(4) 减少对片外存储器的访问次数。片外存储器相对于片内存储器是低速设备,片外存储器的读取时间是片内存储器的几倍至十几倍。对于片外存储器的数据要做到一次读取,完成多次计算。
Blackfin芯片的开发环境VisualDSP本身已经带有汇编器,但由于种种原因,对于某些运算量大、调用频繁的函数仍需要进行手动汇编优化。进行汇编优化时,应注意以下几点:
(1) 节省寄存器资源。Blackfin533提供了8个32位数据寄存器以及一系列的地址寄存器。对于这些寄存器,应尽可能做到一个寄存器多次使用;同时在能用较短的数据类型的情况下用短的数据类型,如能用short则不用int,这样每个32位寄存器可以作为两个16位寄存器使用,相当于增加了寄存器的数量。
(2) 使用专用指令。Blackfin533提供了求最大值、最小值、绝对值、CLIP及大量视频专用指令,通过使用这些指令,能大大提高代码的执行速度。
(3) 使用并行指令。对于大多数指令都存在相对应的并行指令,如一条运算指令可以并行两条数据读取指令。并行指令的使用能成倍提高代码的执行速度。
(4) 将内层循环展开等。
对于不同的图像帧(I、P),各模块所占的比例各不相同。对于I帧,帧内模式选择和去块效应滤波占较大的比例;对于P帧,帧间模式选择则占较大的比例。总之,模式选择及去块效应滤波是H.264编码的瓶颈,需要对这两部分进行优化。
进行模式选择时会调用绝对差值求和函数(SAD)及hadamard变换后再绝对值求和函数(SATD)。这两个函数虽然较简单,但调用较频繁,对这两个函数进行汇编优化,能较大提高模式选择的速度。对于绝对差值求和函数(SAD),通过使用Blackfin的专用视频指令SAA,可以大大提高运算速度。
Reference:
http://en.wikipedia.org/wiki/H.264/MPEG-4_AVC
http://en.wikipedia.org/wiki/Advanced_Audio_Coding
http://www.docin.com/p-62954420.html
http://hopesealy.blog.sohu.com/81461022.html
http://houh-1984.blog.163.com/
本文主要介绍了基于DSP平台的音视频编解码算法的C语言优化方法,如常用的内联函数,数据打包SIMD,函数组合,数据的reuse重用,循环loop的优化,针对结构体和宏定义的优化,对条件控制语句的优化,并以ADI Blackfin DSP处理器上的H.264编码器优化思路。