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)。