MPEG-4/H.264视频编解码工程实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.4 Xvid的MMX/SSE技术优化

上一节对Xvid的系统框架做了功能剖析和技术说明,现对最底层的模块如DCT/IDCT、Q/IQ、VLC/VLD、SAD等做汇编优化和设计。首先概述MMX/SSE2的汇编编程、汇编指令结构等,然后基于多媒体汇编指令优化核心模块。

由于算法系统是C语言编程,而在使用汇编优化核心模块的时候,就涉及了混合编程技术。汇编编写的函数要被C语言调用,则在编程时要遵守一定的规则。如汇编函数名称前加下画线“_”等。

3.4.1 MMX/SSE汇编指令概述

1.MMX媒体扩展指令

MMX技术是对Intel体系结构(IA)指令集的扩展,该技术使用了单指令多数据技术(SIMD),以并行方式处理多个数据元素,从而提高了多媒体和通讯软件的运行速度。MMX指令集包含了57条新指令和一个新的64位四字数据类型。新的64位数据保持了可供MMX指令操作的成组数据值,如图3-12所示。

图3-12 新的数据类型

这样一条MMX指令能够同时处理8/4/2个数据单元,这就是所谓的“单指令多数据”SIMD结构,这种结构是MMX技术把机器性能提高的最根本因素。为了方便使用64位打包整形数据,MMX技术增加了8个64位的MMX寄存器(MM0~MM7),只有MMX指令可以使用MMX寄存器,每个寄存器可按名称直接访问。

值得一提的是,MMX寄存器是随机存取的,但实际上是借用了8个浮点数据寄存器实现的。浮点处理单元FPU有8个浮点寄存器FPR,以堆栈方式存取。每个浮点数据寄存器有80位,高于16位用于指数和符号,低于64位用于有效数字。MMX利用其64位有效数字部分用做随机存取的64位的MMX寄存器。MMX技术对操作系统完全透明,并且与现有的、基于Intel体系结构的软件百分之百兼容。所有应用软件可继续在具有MMX技术的处理器上正常运行。

MMX技术提高了很多应用程序的执行性能,例如活动图像、视频会议、二维图形和三维图形。几乎每一个具有重复性和顺序性整数计算的应用程序都可以从MMX技术中受益。对于8位、16位和32位数据元素的处理,改善了程序的性能。一个MMX指令可一次操作8个字节,且在一个时钟周期内完成两条指令,也就是说,可在一个时钟周期内处理16个数据。

另外,为增强性能,MMX技术为其它功能释放了额外的处理器周期。以前需要其他硬件支持的应用程序,现在仅需软件就能运行。更小的处理器占用率给更高程度的并发技术提供了条件,在当今众多的操作系统中上述并发技术得到了利用。在基于Intel的分析系统中,某些功能的性能提高了50%到400%。这种数量级的性能扩展可以在新一代处理器中得到体现。在软件内核中,其速度得到更大的提高,其幅度为原有速度的三倍速至五倍。

1)MMX指令集分类

表3-2列出了MMX的指令集分类。从功能上可分为7组:算术运算指令、比较运算指令、类型转换运算指令、逻辑运算指令、移位运算指令、数据传送指令和状态清除。

表3-2 MMX指令集分类

2)CPU寄存器

除了上述的8个MMX寄存器,CPU还有自己的寄存器EAX、EBX、ECX、EDX,除了能够直接访问外,还可分别对其高16位和低16位进行访问。它们的低16位就是把它们前面的E去掉,即EAX的低16位就是AX。而且它们的低16位又可以分别进行8位访问,也就是说,AX还可以再进行分解,即AX还可分为AH(高8位),AL(低8位)。另外,CPU还有几个寄存器:ESI、EDI、ESP、EBP,CPU寄存器及其功能描述如表3-3所示。

表3-3 CPU寄存器

其中ESP和EBP一般是针对堆栈而言。

3)地址加载指令

● LEA指令(Load Effective Address)

LEA指令把源操作数的地址偏移量传送给16位通用寄存器。该指令常用来建立操作所需要的寄存器地址指针,格式为:

    LEA r,src             ;src代表源操作数,r代表寄存器。

例如:

    LEA BX,[2400H]        ;将2400单元(偏移地址)送BX,指令执行后BX=2400H
    LEA SI,TABLE         ;把TABLE(偏移地址)送SI,执行后SI指向TABLE;该指令和MOV SI,
    OFFSET TABLE的作用一样。

● LDS指令(Load Pointer Into DS)

LDS指令是把一个存放在4个存储单元中共为32位的目标指针(段地址和偏移量)传送到两个目的寄存器,其中后两个字节(高地址)内容,即段地址送到DS;前两个地址(低地址)内容,即偏移量送到指令中所出现的寄存器中,例如:

    LDS DI,[2130H]             ;把2132H,2133H单元中的内容送到DS;把2130H,213lH单元中的内容

送到DI。

    LDS SI,DWORD PTR CS:[BX] ;把代码段BX指向的四个单元内容送到DS和SI,其中[BX+2],[BX+3]送
    到DS,[BX][BX+1];送到SI,DWORD PTR是属性运算符,它指出是双字(4个字节)。

● LES指令(Load Pointer into ES)

这条指令除把目标段地址送到ES外,其他与LDS相同。LDS/LES指令的源操作数都来自于存储器。

2.SSE/SSE2媒体扩展指令

上面介绍了MMX指令的多媒体处理过程,MMX寄存器的使用注意事项等。微软看到MMX指令大大的提高了程序处理速度,于是相继推出了基于MMX的新的汇编指令SSE、SSE2,把寄存器从64扩展到128位,并且又增加了新的媒体处理汇编指令。

1)SSE/SSE2数据结构

SSE技术支持的主要数据类型是打包的单精度浮点操作数(Packed Single-precision Floating-point)。它是将4个互相独立的32位单精度(SP:Single-Precision)浮点数据,打包在一个128位的数据中。32位单精度数据格式符合IEEE 754标准。SSE技术提供了8个128位的SIMD浮点数据寄存器。每个SIMD浮点数据寄存器都可以直接存取,寄存器名为XMM0 ~ XMM7。它用于存放数据而不能用于寻址存储器。SSE技术还提供了一个新的控制/状态寄存器MXCSR。

SSE2指令系统包括IA-32微处理器原有的32位通用寄存器、64位MMX寄存器、128位XMM寄存器,还包括32位的标志寄存器EFLAGS和浮点状态/控制寄存器MXCSR;但并没有引入新的寄存器和指令执行状态。它主要利用XMM寄存器新增了一种128位打包的双精度浮点数据和4种128位SIMD整型数据类型。

● 打包的双精度浮点数(Packed Double-precision floating-point):

这个128位数据类型由两个符合IEEE标准的64位双精度浮点数组成,打包成一个双4字数据。

● 128位打包的整数(128-bit Packed integer):

这4种128位打包整型数据可以包含16个字节整数、8个字整数、4个双字整数或2个4字整数。

SSE2指令系统中最主要的指令就是针对128位和64位操作模式的打包的双精度浮点指令。另外还有64位和128位SIMD整数指令、MMX和SSE技术的128位扩展指令、高速缓存控制和指令排序指令。

2)SSE指令

SSE指令集有70条指令,它们可以分成三组。

● 50条SIMD浮点指令

SSE技术中的50条SIMD浮点指令是SSE指令系统的主要指令,也是Pentium III处理器性能提高的一个关键。它又可以分成几组:数据传送指令、算术运算指令、逻辑运算指令、比较指令、类型转换指令、组合指令、状态管理指令。

● 12条SIMD整数指令

SSE指令集中有12条SIMD整数指令。这是为了增强和完善MMX指令系统而新增加的指令。

● 8条高速缓冲存储器优化处理指令

为了更好地控制Cache的操作,提高程序运行性能,SSE技术针对Pentium III设计了8条高速缓存的优化处理指令。

3)SSE2指令

SSE2指令系统中最主要的指令就是针对128位和64位操作模式的紧缩双精度浮点指令。另外还有64位和128位SIMD整数指令、MMX和SSE技术的128位扩展指令、高速缓存控制和指令排序指令。

● SSE2浮点指令

SSE2浮点指令分成多组,有SSE2的传送、算术运算、逻辑运算、比较、组合和转换指令。

● SSE2扩展指令

SSE2技术除具有双精度浮点指令外,还在原来MMX和SSE技术基础上补充了SIMD扩展整数指令、高速缓存控制和指令排序指令。

3.4.2 MMX汇编指令优化核心模块

Xvid的汇编程序支持NASM汇编器,它是一个为可移植性与模块化而设计的一个80×86的汇编器。它支持相当多的目标文件格式,包括Linux和NetBSD/FreeBSD、a.out、ELF、COFF、微软16位的OBJ和Win32。它还可以输出纯二进制文件。它的语法设计得相当简洁易懂,和Intel语法相似但更简单,它支持Pentium、P6、MMX、3DNow!、SSE和SSE2等多媒体媒体指令集。

1)定义公共宏

下面的宏定义中使用了宏汇编中的表达式操作符%,它把后面的文本解释为表达式,功能是取表达式的最终值。使用这个操作符可以把表达式的值作为实参引用,而不是引用表达式文本本身。

%macro表示定义多行宏。%endmacro表示多行宏的结束。在%macro一行上宏名后面的数字1定义了宏可以接收的参数的个数。宏定义里面的%1是用来引用宏调用中的第一个参数。对于一个有多个参数的宏,参数序列可以这样写:%2,%3等。%define是定义单行宏。NASM的详细开发技术请参考Nasm中文手册。

    BITS 32                                      ; 表明32位指令
    %macro cglobal 1                              ;cglobal宏定义
        %ifdef PREFIX
            %ifdef MARK_FUNCS
                global _%1:function %1.endfunc-%1
                %define %1 _%1:function %1.endfunc-%1
            %else
                global_%1                       ;有效定义,定义全局函数
                %define %1_%1                    ;在汇编中,使用没有“_”的函数名称
            %endif
        %else
            %ifdef MARK_FUNCS
                global %1:function %1.endfunc-%1
            %else
                global %1
            %endif
        %endif
    %endmacro

上面使用宏定义了函数的名称,宏名为cglobal,由于在汇编文件的汇编选项设置中定义了PREFIX,而没有定义MARK_FUNCS。所以宏的实际定义为上述代码中的加黑显示内容。例如:

global _myfunc

define myfunc _myfunc

2)只读段.rodata存放常量。

    ;=============================================================================
    ; 只读数据段(Read Only Data)
    ;=============================================================================
    %ifdef FORMAT_COFF
    SECTION .rodata
    %else
    SECTION.rodata align=16     ;只读数据段,16字节(128位)对齐
    %endif
    ALIGN 16                  ;16字节对齐
    mmx_one:
        dw 1,1,1,1             ; 定义的字

上述代码定义了16字节对齐的常量mmx_one,4个常量中每个常量为长度16比特,数据放置在只读段.rodata。

3)C语言函数transfer_8to16copy_c的功能把像素值从8位扩展为16位。

    void
    transfer_8to16copy_c(int16_t*const dst,  const uint8_t*const src,  uint32_t stride)
    {
        uint32_t i, j;
        for (j = 0; j < 8; j++) {
            for (i = 0; i < 8; i++) {
                dst[j * 8 + i] = (int16_t) src[j * stride + i];
            }
        }
    }

上述代码两层循环,每次处理一个单元,即把8位的像素值扩展为16位。

4)MMX的寄存器是64位,这样MMX指令可以一次处理8个字节。该函数的汇编优化是transfer_8to16copy_mmx(),该函数优化的思路是把要复制的数据打包形成64位,每次存储两行,执行四次宏处理。下面是汇编优化的结果。

    SECTION.text                        ;.text 段
    cglobal transfer_8to16copy_mmx       ; 函数声明:_transfer_8to16copy_mmx
    %macro COPY_8_TO_16 1                 ; 定义宏COPY_8_TO_16开始
      movq mm0,[eax]                     ; 取64bit,第一行8个像素点
      movq mm1,[eax+edx]                 ; 取64bit,第二行8个像素点
      movq mm2,mm0                       ;mm2=mm0
      movq mm3,mm1                      ;mm3=mm1
      punpcklbw mm0,mm7                 ;mm0的低4 byte扩展成word
      movq[ecx+%1*32],mm0              ;mm0的内容存到[ecx+%1*32]
      punpcklbw mm1,mm7                 ;mm1的低4byte扩展成word
      movq[ecx+%1*32+16],mm1           ;mm1的内容存到[ecx+%1*32+16]
      punpckhbw mm2,mm7                 ;mm2的低4byte扩展成word
      punpckhbw mm3,mm7                 ;mm3的低4byte扩展成word
      lea eax,[eax+2*edx]               ;修改eax值,指向当前行的第三行
      movq[ecx+%1*32+8],mm2            ;mm2的内容存到[ecx+%1*32+8]
      movq[ecx+%1*32+24],mm3           ;mm3的内容存到[ecx+%1*32+24]
    %endmacro                          ; 定义宏COPY_8_TO_16结束
    ALIGN 16                            ; 下面地址是16字节对齐
    transfer_8to16copy_mmx:            ; 实际是_transfer_8to16copy_mmx,前面有define定义。
      mov ecx,[esp+4];Dst               ; 取第一个参数 dst
      mov eax,[esp+8];Src               ; 取第二个参数 src
      mov edx,[esp+12];Stride           ; 取第三个参数 stride
      pxor mm7,mm7                      ;mm7 清零
      COPY_8_TO_16 0                    ; 第一次展开宏,处理0、1行
      COPY_8_TO_16 1                    ; 第二次展开宏,处理2、3行
      COPY_8_TO_16 2                    ; 第三次展开宏,处理4、5行
      COPY_8_TO_16 3                    ; 第四次展开宏,处理6、7行
      ret                              ; 函数返回
    .endfunc

经过分析,上述汇编函数的显式定义如下,可以看出汇编函数名称前必须加下画线“_”,以保证能够被C语言调用。传入的参数在堆栈中,使用ESP指针加偏移量读取。

    ;--------------------------------------------------------------------------------------------
    ; void transfer_16to8copy_mmx(uint8_t * const dst,
    ;                         const int16_t*const src,
    ;                         uint32_t stride);
    ;-------------------------------------------------------------------------------------------
      SECTION .text
      global _transfer_8to16copy_mmx
    ALIGN 16
    _transfer_8to16copy_mmx:
      ;汇编指令
      ;汇编指令
      ret
    .endfunc

上述代码是函数transfer_16to8copy_mmx的汇编实现。汇编中函数名称前加了下画线“_”。使用global关键字声明该函数为全局函数,使得C语言能调用该函数。

5)其他有的核心模块也是使用了MMX汇编指令做优化。开发过程同transfer_8to16copy_mmx类似。详情参考xx_mmx.asm文件,所有模块的优化均使用了MMX指令。关于MMX指令的开发请读者参阅其他有关书籍。

3.4.3 SSE2汇编指令优化核心模块

还有一部分核心模块使用了SSE2汇编指令做优化。SSE2汇编语言的数据寄存器是128位,共8个XMM0~XMM7。这样SSE指令可以一次处理16个字节,相比MMX指令的一次处理8字节,效率提高了一倍。

1)MMX和SSE2的一般宏定义相同,常量同样放在.rodata段下,地址16字节对齐。

2)下面以SAD模块的SSE2汇编优化为案例,分析SSE2汇编指令优化过程,优化思路是每次处理宏块的两行,执行8次宏处理。

SAD16的C语言实现如下:

    uint32_t sad16_c(const uint8_t * const cur, const uint8_t * const ref,
            const uint32_t stride,   const uint32_t best_sad)
    {
        uint32_t sad = 0;
        uint32_t i, j;
        uint8_t const*ptr_cur=cur;      //当前帧指针
        uint8_t const*ptr_ref=ref;      //参考帧指针
        for(j=0;j<16;j++){             //外循环
            for(i=0;i<16;i++){         //内循环
                int pixel=(ptr_cur[i]-ptr_ref2[i]);     //对应位置相间
                sad+=abs(pixel);                       //累加绝对值
            }
            ptr_cur+=stride;           //指向一下行
            ptr_ref+=stride;           //指向一下行
        }
        return sad;                     //返回sad值
    }

上述程序是计算对应块的SAD值,两层循环,每次只计算一个像素点的差值。先采用SSE的指令优化该函数。下面代码是该函数的SSE优化结果。

    ;------------------------------------------------------------------------------------------
    ; uint32_t sad16_sse2 (const uint8_t * const cur, <- assumed aligned!
    ;                   const uint8_t*const ref,
    ;                    const uint32_t stride,
    ;                   const uint32_t/*ignored*/);
    ;----------------------------------------------------------------------------------------
    %macro SAD_16x16_SSE2 0         ;定义宏
      movdqu  xmm0,[edx]          ;未16字节对齐读取,=>xmm0
      movdqu  xmm1,[edx+ecx]      ;未16字节对齐读取,=>xmm1
      lea edx,[edx+2*ecx]          ;修改edx
      movdqa  xmm2,[eax]          ;16字节对齐读取,=>xmm2
      movdqa  xmm3,[eax+ecx]      ;16字节对齐读取,=>xmm3
      lea eax,[eax+2*ecx]          ;修改eax
      psadbw  xmm0,xmm2           ;8个字节的SAD值,结果放置在低16位
      paddusw xmm6,xmm0            ;8个无符号16位饱和相加
    psadbw               xmm1,xmm3       ;8个字节的SAD值,结果放置在低16位
    paddusw xmm6,xmm1                     ;8个无符号16位饱和相加
  %endmacro                              ;宏定义结束
  ALIGN 16                                ;16字节对齐
  sad16_sse2:                            ;函数声明,_sad16_see2
    mov eax,[esp+4];cur(assumed aligned)             ;取第一个参数,cur是16字节对齐
    mov edx,[esp+8];ref                             ;取第二个参数,ref对齐不限制
    mov ecx,[esp+12];stride                         ;取第三个参数
    pxor xmm6,xmm6;accum                            ;xmm6=0
    SAD_16x16_SSE2                                 ;宏展开,处理0、1行
    SAD_16x16_SSE2                                 ;宏展开,处理2、3行
    SAD_16x16_SSE2                                 ;宏展开,处理4、5行
    SAD_16x16_SSE2                                 ;宏展开,处理6、7行
    SAD_16x16_SSE2                                 ;宏展开,处理8、9行
    SAD_16x16_SSE2                                 ;宏展开,处理10、11行
    SAD_16x16_SSE2                                 ;宏展开,处理12、13行
    SAD_16x16_SSE2                                 ;宏展开,处理14、15行
    pshufd              xmm5,xmm6,00000010b        ;根据00000010b重排xmm6
    paddusw xmm6,xmm5                               ;8个无符号16位饱和相加
    pextrw              eax,xmm6,0                 ;扩展字,前面补零
    ret
  .endfunc                                         ;函数声明结束

上述代码中,宏SAD_16x16_SSE2处理16×16宏块的两行,16个单字节的像素存储于SSE的一个128位寄存器XMM中。调用8次宏,完成整个宏块的处理。

总之,MMX或SSE2汇编指令,其优化的根本思路是数据打包、单指令处理多数据源(SIMD)。