H.264算法的优化策略

文章来源: http://www.tichinese.com/Article/Video/200909/2150.html 编辑:小乙哥

1 代码优化的主要方法

通过代码移植能够获得在DSP上初步运行的代码,但是它由于没有考虑到DSP自身的硬件特点,不适合DSP强大的并行处理能力,因此执行效率低下,不能满足我们的实时要求,需要对其进行进一步优化。

对DSP代码进行优化的手段有以下三个层次,分别是:项目级(Project)优化,算法级(algorithm)优化,指令级(instruction)优化。下面对这三种优化手段分别进行介绍。

1)项目级优化

项目级优化,是对项目的整体优化,主要手段有以下几点:
首先是在对整个项目进行编译链接生成DSP代码时,合理选择配置编译器选项,并针对这些参数选择,对程序进行调整和修正。其中进行的工作有:

  • 项目编译时,通过-o3选项来调用最高级别的软件流水线优化,通过-mw选项来调用软件流水线循环反馈,从而增大软件编译成DSP代码的并行性。
  • 项目编译时,用-pm,-o3和-mt选项来改善循环,多重循环,庞大循环体循环的性能。
  • 只读变量声明成const型,循环计数器定义为int型,从而加大DSP代码的并行性。

其次对程序结构进行调整,对不适合DSP执行的语句进行改写,以提高代码的并行性。例如,DSP处理器并行性很高,能够对代码进行流水线处理,但是原始代码存在大量条件判断语句,会对流水线造成中断,不利于代码的并行处理,因此,可以采用判断提前,去除不必要的判断等方式减少判断语句对流水线的中断。

2)算法级优化

算法级优化,就是利用H.264的自身特点,采用一些快速算法,在不影响编码质量的前提下,提高编码器速度,从而在速度和质量上达到一个较好的平衡。

3)指令级优化

上述方法无法达到要求时,就要进行指令级优化。C64x系列DSP有丰富的具有高度并行性处理能力的指令。下面介绍一些C64x系列DSP媒体处理相关指令。

  • ADD4:加法指令,一次执行4对8位数的加法。一个寄存器有32位,可以存放4个8位数据。计算中,两个源寄存器中的四组对应8位数据 分别相加,结果存放在目标寄存器中。
  • AVGU4:一次执行4对8位无符号数据求平均运算。计算中,两个源寄存器中的4组8位无符号型紧缩字求平均,结果以4个8位紧缩字的 形式存放在目标寄存器中。
  • DOTPU4:一次执行4对8位无符号数据点乘运算。计算中,两个源寄存器中的4组8位无符号型紧缩字对应相乘,乘积相加,所得结果存放 在32位寄存器中
  • SUBABS4:一次执行4对8位无符号数据求差绝对值运算。计算中,两个源寄存器中的4组8位无符号型紧缩字对应相减,差值求绝对值,所得结果以4个8位紧缩字的形式存放在目标寄存器中。
  • LDB/LDH/LDW/LDDW:将8位,16位,32位或64位数据读入目标寄存器中,所读取的数据在内存中是地址align(32位对齐)的数据。
  • LDNW/LDNDW:将一个32位或64位的非对齐数据读入目标寄存器中。
  • STB/STH/STW/STDW:将8位,16位,32位或64位数据写入内存中,所写入的数据在内村中是地址align(32位对齐)的数据。
  • STNW/STNDW:将一个32位或64位的非对齐数据写入内存中。

除了这些高并行度的指令,TI还提供了丰富的算法库[37],如Image/Video Processing Library图像/视频处理库(IMGLib),Digital signal processor Library数字信号处理库(DSPLib)等。这些算法库中的函数都是已经充分优化过的算法模块,而且大都提供对应的C、线性汇编和汇编源代码,并有文档进行API介绍。所以要充分利用。

2 算法关键模块的优化

对模块的优化分三步进行。先认真分析代码,并进行相应的调整,例如尽量减少有判断跳转的代码,特别是在for循环中,因为判断跳转会打断软件流水。可以用查表或者用_cmpgtu4、_cmpeq4等intrinsic来代替比较判断指令,从而巧妙地替代判断跳转语句。同时还可以采用TI的CCS中所提供的#pragma,为编译器提供尽量多的信息。这些信息包括for循环的次数信息、数据对齐信息等。如果经过这部分优化后还无法满足系统要求,则对这部分模块使用线性汇编来实现。

2.1 整数变换和量化

整数变换和反变换的运算步骤见下表:

图1 DCT与IDCT的运算步骤

整数运算和反变换的次数极多,比如,在D1格式下,4?4DCT等要做21600次,如果算上其他大小宏块的这些变换,时间消耗还是很多的,所以仍然有优化的必要。下面以整数变换为例介绍如何用线性汇编对C语言代码进行优化:

1)预测残差的输入

DCT和IDCT优化的关键是在数据的读和写上。读写指令是和存储器操作相关的,所以要尽量减少其操作的次数。优化时可以用LDNW(无边界调整字(四字节)读取)和STNW(无边界调整字(四字节)存储)来代替单字节读取和存储,然后在寄存器内部再对数据进行打包处理,这样可以大大提高速度。读入方法如下:

LDNDW *A4, a1:a0 ;第一行数据

LDNDW *+A4(8), a3:a2 ;第二行数据

LDNDW *+A4(16), b3:b2;第三行数据

LDNDW *+A4(24), b1:b0;第四行数据

寄存器A4是系统指定用来保存函数返回值的。8字节的数据的读入要保存到寄存器对中。

2)行变换的实现

C64x里面有多种加法指令,寄存器内容可作为32位数据相加,也可作为两个16位数据相加,也可以作为四个8位数据相加。对应于上面的数据输入方法,我们采用16位相加,并使用.S功能单元。实现方法如下:

加法指令

ADD2 a0, b0: r_00 ;a + d

ADD2 a1, b1: r_01 ;

ADD2 a2, b2: r_10 ;b + c

ADD2 a3, b3: r_11 ;

减法指令

SUB2 a2, b2: r _20 ; b - c

SUB2 a3, b3: r_21 ;

SUB2 a0, b0: r_30 ; a - d

SUB2 a1, b1: r_31 ;

另外,因为在C64x指令中没有对两个16位数据同时左移的指令,同时DM642的乘法指令可 以在一个周期内完成,所以在表1中求B, D时要使用MPY2指令实现向左的移位运算。由于乘法运算后数据的位数要进行扩展,因此MPY2的结果需要放入一个寄存器对,但实际有效数据不会超过16 位,因此完成乘法后我们又把数据两两打包在一个寄存器里,以方便在下面的列变换中进行数据的并行处理。实现方法如下:

MPY2 r_30, r _t,r_tl:r_ t0 ;2*(a-d) r_ t=0x00020002

MPY2 r-31, r_t,r_t3:r_t2

PACK2 r_t 1, r_t0, r_t0

PACK2 r_t3, r_t2, r_t2

3)列变换的实现

行变换后数据以行优先方式存放在4个寄存器对内。所以在进行列变换前要对寄存器内的数据进行转置调整,以方便我们使用上述的数据并行处理指令。

第一列

PACK2 .S1 a2, a0, r_ m00

PACK2 .S2 b0, b2, r_ m01

第二列

PACKH2 .Ll a2, a0, r_ m10

PACKH2 .L2 b0, b2, r ml l

第三列

PACK2 .S1 a3,a1,r -m20

PACK2 .S2 b1,b3,r_ m21

第四列

PACKH2 .Ll a3,a1,r -m30

PACKH2 .L2 b1,b3,r_ m31

经过寄存器中数据的转置处理后,我们就可以使用与行变换类似的程序进行列变换的实现。

4)输出变换结果

列变换的结果即为整数变换的结果,但是输出之前必须进行第二次转置处理,使得输出数据仍然为行优先方式存储。

STNDW r_mOut0l:r mOut00, *A4

STNDW r_mOut1l:r mOut10, *A4(8)

STNDW r_mOut2l:r mOut20, *A4(16)

STNDW r_mOut3l:r mOut30, *A4(24)

5)检查生成的汇编代码并对线性汇编代码进行相应的调整,以提高效率。如,避免使用A16~A31和B16~B31寄存器,因为这两组寄存器需要被保护,如果被使用,编译器会花额外的时间对这些寄存器进行保护操作,这会打断流水,降低效率。

线性汇编代码中用“.cproc” 和“.endproc”命令限定了需要优化器优化的代码段,“.reg”命令允许使用将要存入寄存器的数值描述名字,也就是为寄存器设定了一个标识符。寄存器A4是系统指定用来保存函数返回值的。

量化(DCT)和反量化(IDCT)与整数变换相比多了if判断,如下所示:

for(i = 0 ; i < 16 ; i ++)

{

if (data[i] > 0)

{

data[i] = (data[i] * quant[mf_index][i] ) >> qbits;

}

else

{

data[i] = -(-(data[i] * quant[mf_index][i]) >> qbits);

}

}

而判断指令会打断软件流水,所以应该避免使用。观察代码可知,进行移位操作的都是正数,所以优化时可以对绝对值进行操作,而把符号放在另外的存储器中,移位计算后再将符号加上去。

比较线性汇编优化前后的性能,我们可以发现效果相当明显。

图2 优化前后比较

DCT和量化实际上是两个相关联的部分,总是成对出现,我们可以把DCT和量化一起完成,这样数据可以在寄存器中完成运算,节省了数据存储和读取时间。

2.2 熵编码和解码

2.2.1 查表方法的改进

CAVLC编码按如下步骤进行:

1.编码非零系数的个数(TotalCoeffs)和TrailingOnes的个数(Coeff_token)

通过查表进行,表有5个,定义在结构h264_coeff_token[5][17*4]中,选择码表的依据是当前块上面和左面块的非零系数个数(N0和N1)。由这两个值计算一个参数N来查表。这就是所谓的基于上下文自适应(context adaptive)。

2.对每个TrailingOnes的符号进行编码

对于每个TrailingOnes都要用一个比特来表示它的符号 (0代表正,1代表负)。符号的编码是逆序进行的,即从最高频的TrailingOnes开始编码。

3.对除TrailingOnes之外的非零系数的level值进行编码

当前块中每个余下的非零系数的level值 (符号和绝对值) 都将按照逆序进行编码,即从最高频的系数开始到DC系数结束。编码每个level所用的查找表是依据先前己编码系数的level的绝对值来决定的(上下文自适应)。按原来的做法,此处将查7个表Level_VLC0到Level_VLC6。Level_VLC0适合编码较小的绝对值;Level_VLC1 适合编码稍大的绝对值,依此类推。现在采用一种新的查表方式,将非零系数的幅值(Levels)分成两部分:前缀(level_prefix)和后缀(leve_suffix)。前缀和后缀的求法和一个变量suffixLength有关,如下公式:

变量suffixLength是基于上下文模式自适应更新的,它的更新与当前的 suffixLength的值以及已经解码好的非零系数的值(Level)有关。这样可通过更新suffixLength的值将各种非零系数的前缀都控制在一个较小的范围内,这样既有利于码表的构建又有利于提高查表的速度,更新后的算法仅需要一个level_prefix的码表,码表如下。

图3 level_prefix码表

文章来源: http://www.tichinese.com/Article/Video/200909/2150.html 编辑:小乙哥
level_suffix的编码只需先按照公式4.1计算出数值,然后根据suffixLength的值来确定后缀的长度即可,无需另外建立码表。

总体来说,更新的查表方法有以下两个特点:

1) 大的码表分解成若干小码表,缩小了搜索范围。

2) 利用小码表中码字的规律快速确定参数在码表中的位置。

4.对TotalZero进行编码

TotalZero代表DC系数与最高频的非零系数之间零系数的个数。之所以要单独对TotalZero进行编码是因为在系数序列的低频部分往往会含有多个非零系数,而如果编码TotalZero就可以避免去编码系数序列低频部分的一些零游程。

5.对每个run-before进行编码

run-before表示当前非零系数与下一个非零系数之间零系数的个数。 run-before需要按照逆序编码。从高频系数开始,每个非零系数的run-before都要进行编码,但有两个例外:

(1)如果没有余下的零系数需要编码,则没必要再编码任何zero-run( 即之前编码的run-before之和等于TotalZero)。

(2)没有必要对最后一个 (最低频) 非零系数的run-zero进行编码。

至此,完成了CAVLC编码。

在熵解码时,TotalCoeff、TrailingOnes、TotalZeros和 Run_before的值都是直接查表求的,过程比较简单,此处不进行介绍。在解码除拖尾系数之外的非零系数幅值时先从当前解码位置起逐个比特进行检测,直到找到第一个比特“1”为止。此比特之前,经检测为“0”的比特的个数就是前缀值。通过前缀值找到对应的码表,并决定后缀的长度。最后读入后缀值。利用上面公式反向计算就可得到非零系数的幅值。

2.2.2 函数的优化

CAVLC编码前要将4×4的残差块转换成zig-zag排列,如图4所示。

图4 4×4亮度的zig-zag扫描

Zig-zag扫描后,按步骤是先对扫描后数据的进行逆序遍历,求出非零系数和拖尾系数的数目之后再开始编码写码流。

函数调用时,要将PC和一些寄存器压栈保存,函数返回时,则要将这些寄存器出栈返回,增加了一些不必要的操作,因此要对这部分函数进行修改。

C代码中,zig-zag扫描是由一组赋值语句完成的,将顺序排列的16个数的数组转换成 zig-zag顺序排列的数组。编译器将这段代码转化为汇编代码是将16个数读入寄存器后再进行排列后写回的,这时要读写一次寄存器,之后遍历数组求非零系数和拖尾系数时又要读写一次寄存器。我们可以将zig-zag扫描的函数改写成线性汇编代码,将数据读入到寄存器后就进行遍历,另外用寄存器作计数器,记下非零系数和拖尾系数的数目,然后将zig-zag排列的数据和求得的系数一次写回,这样就省掉了一次寄存器的读写,多次累计起来省掉的时间开销还是相当可观的。

CAVLC编码和解码中多次调用写码流函数和读码流函数,由于调用次数很多,可以将这些函数表示成内联函数,优化效果也相当明显。比如,完成100次C代码的写码流函数BitstreamPutBits花费4075个cycle,而表示成内联函数后仅需要2600个cycle。

2.3 帧内预测

2.3.1 帧内4×4 预测模式判别方法的改进

计算4×4 预测模式的过程就是对4×4 块的9 种预测模式进行计算的过程,分别求出各个模式的SAD值,选取SAD 值最小的预测模式为最优预测模式,因此编码器的计算复杂度很高。

参考相关文献可以对判别方法进行如下改进:由于在H.264 中,残差矩阵量化前后的变换系数都是整数,只要4×4 变换前的残差矩阵满足如下条件,其残差系数量化后的值就是全零。

公式1

其中 为残差矩阵对应位置的值,,QE是量化参数矩阵,对于给定的为中的最小值。根据相邻块的预测模式预测当前4×4 块的最可能预测模式为predmode。根据此预测模式取其残差矩阵,对其残差做,并判断是否。如果不等式成立,则当前最可能的预测模式predmode 就是最优预测模式bestmode,结束此4×4 块的预测模式判断。否则,对此4×4 块的其他预测模式进行判断。对其中的一种预测模式计算它的预测残差矩阵,判断不等式是否成立,如果成立,则此预测模式为此前4×4 块的最优预测模式bestmode,结束此4×4 块的预测模式判断。否则对此4×4 块的其他预测模式进行同样处理,直至9 种预测模式都进行了判断,获得一个最优的预测模式。

同时,在改进算法中,对于那些判断残差进行变换量化后结果为全零的4×4 块进行了特别处理。由于这些4×4 块的残差经过变换量化后结果为全零,对它们的残差再进行变换、量化、反量化和反变换就失去了意义,因此可以对这些处理过程进行简化。具体操作为:对这些 4×4块的残差矩阵直接全赋值为零,并对Z 扫描的结果也同样赋值为零,从而省去了变换、量化、反变换和反量化,并且对编码结果和图像的重构结果都不会有影响,能够进一步减少计算复杂度。

2.3.2 亮度预测的并行实现和线性汇编优化

4×4子块亮度预测除在汇编指令级采用超长指令字VLIW并行执行8条指令外,主要采用数据并行、线性汇编优化和使用软件流水进行优化处理。

4×4子块亮度预测时的数据都是0~255,即unsigned char,在DSP中占一个字节:采用寄存器组A、B并行处理4×4子块前两行与后两行两块数据,采用寄存器高低位并行处理两行数据,如图5所示。这样并行处理得到近似为4的加速比。

图5 4×4亮度块预测数据并行

观察4×4子块亮度预测的九种预测模式,我们会发现后6种预测模式16个像素点的预测有很强的相关性,使用软件流水线可以得到较好的效果。以模式3 (左下对角线模式)为例,16个像素点分别由公式2得出。

公式2

线性汇编的编写过程如下:

1.用四字节的读写指令代替单字节的读写指令

利用LDNW指令用两次将ABCD和EFGH读入到两个寄存器中,利用STNW指令分四次将数据存储。

2.重新组装寄存器里的数据

ABCD和EFGH并不是分开的,总是四个一组进行计算。公式4.2中1、2式的操作数是 ABCD,3、4式是CDEF,5、6、7式是EFGH。因而我们不需要将ABCD与EFGH完全拆开存储,只需要将ABCD中的高半字和EFGH中的低半字拆出来存到一个独立的寄存器中。这条指令是PACKHL2。

3.分析运算式规律,选择最简单的运算指令

所有的运算中,都存在一个计算式X+2Y+Z,同时有三个unsigned char型数据参加运算,下面一条C6400系列的汇编指令正好可以用于此处。

DOTPU4 (.M) src1, src2, dst

这条指令求src1和src2中的两个无符号数的4个字节对应的积,再相加,和数送入dst 中。在这里我们可以用src2 = 0x0121分别与ABCD、CDEF和EFGH做DOTPU4运算,可直接得出B+2C+D、D+2E+F和F+2G+H,用src2 = 0x1210分别与ABCD、CDEF和EFGH做DOTPU4运算,可直接得出A+2B+C、C+2D+E和E+2F+G。运算后的结果加2后右移2位即可。

对4×4子块亮度预测的后6种预测模式进行优化后,性能有明显提高,如下表所示(-o3,no –mu允许软件流水的条件下)。

font>

图6 优化性能比较

16×16宏块亮度预测基本上与4×4块亮度预测一致,但对早期终止策略进行了优化处理。

16×16宏块亮度预测按照模式0(垂直预测)→模式1(水平预测) →模式2 (直流预测) →模式3 (平面预测) 编码顺序进行,并在每种预测模式中采用早期终止策略后,流程为[34]:16×16宏块第i列数据预测、第i列数据求残差、计算第i列数据SAD值、判别是否进行下一模式预测 (当SAD值大于16个4×4子块的最优模式下的SAD之和min_cost时)。这样最多会增加了15次判断,有测试中表明83%的宏块在计算了12列数据SAD值才满足判决条件,15%的宏块在计算了16列数据SAD值才满足判决条件,2%的宏块在计算了8列数据SAD值就满足判决条件。故实际编程中仅在计算了12列数据SAD值后增加判决,仅增加一次判决,16×16宏块编码效率提高20.04%。流程见图7所示。

图7 16×16宏块亮度预测流程

2.4 运动估计

在视频编码中,运动估计和补偿起着最为关键的作用,通常占一个压缩编码方案总计算量的60%~ 80%。块匹配法是目前最为广泛应用的运动估计方法。在H. 264 中,运动补偿部分与之前的标准有很大的不同。它支持更大范围的运动补偿块,以达到高精度匹配,充分消除时域冗余度,最大程度减小预测误差。而这是以极高的运算量和复杂度为代价的,仅以整像素搜索为例,H. 264 共允许多种大小的补偿块 (最小可到4×4),对一个16×16 的宏块采用某一种分块方式进行全搜索,搜索范围为16,则求匹配差值的计算次数为:

(2 × 16 + 1) × (2 × 16 + 1) × 16 × 16= 2. 79×105

这样大的计算量显然会给实时视频处理带来巨大困难,所以要寻找快速的搜索算法和判决策略。

H. 264 标准规定对16×16 的宏块可以采用16×16、16×8、8×16、8×8 的分块方式,而对8×8 的分块又可进一步分成8×8、8×4、4×8、4×4 的小块。每个独立的分块分别进行运动搜索,这样的处理固然可以达到最好的匹配效果,但要求巨大的运算量。16×8 和8×16 分块方式的运算量各自与16×16 分块方式的运算量大致相当,而8×8 分块方式由于其每一个小块都要独立作4 种方式的搜索,其总的运算量大致相当于16×16 的4倍。而通过对H.264标准测试序列测试的结果来看,对Foreman 这类运动剧烈的序列,8×8 分块方式为最优模式的宏块数只占总宏块数的不到10% ,对Akiyo这类平缓的序列,8×8 方式的宏块百分比更是在2%,因此在优化中可以放弃8×8这种分块模式。

运动估计的实现采用分步计算加提前终止的方法。其步骤如下:

1.搜索的上下文信息都存放在一个结构体H264_search_context_t 中,context里存储有推荐的运动矢量(通常有5个:标准预测值、上、右上三个块运动向量和0向量),首先尝试context->vec[0] 标准向量,计算RD_cost(这里算得sad),若RD_cost < th0=256,则设置最优vec_best为vec[0],并返回sad值。

2.然后尝试运动向量的其他预测值,找到最小的SAD,若该SAD小于刚才找到的sad,则设置context->vec_best = context->vec[best],如果该SAD < th0,则返回SAD。

3.若上面都没有找到小于阈值的SAD,则调用small_diamond_search,进行小菱形搜索,实际就是搜索上下左右四个点,直到当前点为5个点中SAD的最小点。如下图所示。

图8 小菱形搜索

你可能感兴趣的:(算法)