前面我们测试了高级语言做饱和处理的性能。其实,对于这样的大批量数据处理,使用SIMD(Single Instruction Multiple Data,单指令多数据流)技术能极大的提高性能。MMX指令集是目前x86平台上覆盖最广的SIMD指令集,于是本文对它进行探讨。
本文致力于解决以下问题——
1.MMX指令集是什么?
2.如何阅读Intel/AMD的手册?
2.如何运用MMX指令集?
3.如何在VC++6.0这样的高级语言编译器中使用MMX指令集?
一、MMX指令集简介
MMX(Multi Media eXtension,多媒体扩展指令集)指令集是Intel公司于1996年推出的一项多媒体指令增强技术。MMX指令集中包括有57条多媒体指令,通过这些指令可以一次处理多个数据。
随后不久,其他CPU厂商也加入对MMX指令集的支持,例如AMD从1997年的K6开始就支持MMX指令集。关于Intel和AMD对SIMD指令集的支持状况,详见——
http://www.cnblogs.com/zyl910/archive/2012/02/26/x86_simd_table.html
[x86]SIMD指令集发展历程表(MMX、SSE、AVX等)
1.1 概述
MMX技术对x86体系的编程环境的扩展是——
1.8个MMX寄存器(mm0~mm7)。
2.4种MMX数据类型(紧缩字节、紧缩字、紧缩双字、四字)。
3.MMX指令系统。
1.2 MMX寄存器
MMX寄存器集是由8个64位寄存器组成,见下图。MMX指令使用寄存器名mm0~mm7直接访问MMX寄存器。这些寄存器只能用来对MMX数据类型进行数据计算,不能寻址存储器。MMX指令中存储器操作数的寻址仍使用标准的x86寻址方式和通用寄存器来进行。
(Figure 9-2. MMX Register Set。出自Intel手册 Vol. 1 9-3)
这些寄存器是作为独立寄存器来定义的。但是,它们是通过对FPU(Float Point Unit,浮点运算单元)寄存器堆栈(R0~R7)别名而来的。所以MMX指令与浮点指令不能混用(EMMS指令可清除MMX状态)。
1.3 MMX数据类型
MMX技术定义了以下4种64位数据类型——
1.紧缩字节(Packed byte):8个字节(byte)紧缩成一个64位。
2.紧缩字(Packed word):4个字(word)紧缩成一个64位。
3.紧缩双字(Packed doubleword):2个双字(doubleword)紧缩成一个64位。
4.四字(Quadword):一个64位。
紧缩字节数据类型中,字节的编号为0~7,第0字节在该类型的低有效位(位0~7),第7字节在高有效位(位56~63)。
紧缩字数据类型中,字的编号为0~3,第0字在该类型的低有效位(位0~15),第3字在高有效位(位48~63)。
紧缩双字数据类型中,双字的编号为0~1,第0双字在该类型的低有效位(位0~31),第1双字在高有效位(位32~63)。
MMX指令可以用64位块方式与存储器进行数据传送,也可以用32位块方式与通用寄存器进行数据传送。
注意,在对紧缩数据类型进行算术或逻辑操作时,MMX指令会对64位MMX寄存器中的字节、字、双字进行并行操作。
对紧缩紧缩数据类型的字节、字、双字进行操作时,这些数据可以是带符号整型数据,也可以是无符号整型数据。
二、指令解读
MMX指令集中有57条指令,它们具体的功能是什么呢?这需要阅读Intel和AMD的手册。
例如“将64位像素转为32位像素”这项工作,可以使用PACKUSWB指令。在这以PACKUSWB指令为例,讲解如何阅读Intel和AMD的手册。
2.1 Intel手册对PACKUSWB指令的说明
Intel手册可以在这下载——
http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html
Intel手册有三种下载包——
1个文件:Combined Volume Set of Intel® 64 and IA-32 Architectures Software Developer’s Manuals。
3个文件:3 Volume Set of Intel® 64 and IA-32 Architectures Software Developer’s Manuals。
7个文件:7 Volume Set of Intel® 64 and IA-32 Architectures Software Developer’s Manuals。
这3种随便选择一种都行。我喜欢“Combined Volume Set of Intel® 64 and IA-32 Architectures Software Developer’s Manuals”,因为只需要下载1个文件。
下载完成后,打开该pdf文档。在左侧的书签树中依次展开“Volume 2: Instruction Set Reference, A-Z”、“Chapter 4 Instruction Set Reference, M-Z”、“4.2 Instructions (M-Z)”,然后拖动滚动条找到“PACKUSWB-Pack with Unsigned Saturation”——
Intel手册对PACKUSWB指令的说明有三页,本文将这3页命名为 图1、图2、图3。
图1:Vol. 2B 4-191
图2:Vol. 2B 4-192
图3:Vol. 2B 4-193
PS:MMX编程指南见“Volume 1: Basic Architecture”的“Chapter 9 Programming with Intel® MMX™ Technology”。不属于本文范畴,读者可自行翻阅。
2.2 AMD手册对PACKUSWB指令的说明
AMD手册可以在这下载——
http://developer.amd.com/documentation/guides/Pages/default.aspx#manuals
找到“Manuals”条目,发现AMD手册是分为5卷——
虽然本文只用到第5卷(AMD64 Architecture Programmer's Manual Volume 5: 64-Bit Media and x87 Floating-Point Instructions),但建议读者将这5卷都下载下载。
下载完成后,打开第5卷(26569_APM_v5.pdf)。在左侧的书签树中依次展开“64-Bit Media Instruction Reference”,然后拖动滚动条找到“PACKUSWB”——
图4
AMD手册对PACKUSWB指令的说明有两页,上图(图4)是第一页的内容。第二页内容不属于本文范畴,故不贴图,读者可自行翻阅。
PS:MMX编程指南见“Volume 1: Application Programming”的“5 64-Bit Media Programming”。不属于本文范畴,读者可自行翻阅。
2.3 手册解读
首先看Intel手册,图1最上面的那个方框内,列出了PACKUSWB指令在不同环境下(MMX、SSE、AVX)的效果。
本文只关心MMX指令集。此时该指令的格式为“PACKUSWB mm, mm/m64”,描述信息为“Converts 4 signed word integers from mm and 4 signed word integers from mm/m64 into 8 unsigned byte integers in mm using unsigned saturation.”。
而在AMD手册(图4)中,对该指令的描述是“Packs 16-bit signed integers in an MMX register and another MMX register or 64-bit memory location into 8-bit unsigned integers in an MMX register.”。
可见,Intel与AMD手册对该指令的说明略有不同。这是因为该功能很难用自然语言描述。用伪代码或图片会好一点。
在Intel手册PACKUSWB指令的第二页(图2)中,有解释该指令功能的伪代码——
PACKUSWB (with 64-bit operands) DEST[7:0] ← SaturateSignedWordToUnsignedByte DEST[15:0]; DEST[15:8] ← SaturateSignedWordToUnsignedByte DEST[31:16]; DEST[23:16] ← SaturateSignedWordToUnsignedByte DEST[47:32]; DEST[31:24] ← SaturateSignedWordToUnsignedByte DEST[63:48]; DEST[39:32] ← SaturateSignedWordToUnsignedByte SRC[15:0]; DEST[47:40] ← SaturateSignedWordToUnsignedByte SRC[31:16]; DEST[55:48] ← SaturateSignedWordToUnsignedByte SRC[47:32]; DEST[63:56] ← SaturateSignedWordToUnsignedByte SRC[63:48];
该伪代码的大致含义为——将DEST中的 每16位的带符号整数 饱和转换为 8位的无符号整数,放到返回值(DEST)的低32位;将SRC中的每16位的整数 饱和转换为 8位的整数,放到返回值的高32位。
注:x86指令的2操作数指令一般是——第1个参数是DEST,第2个参数是SRC。即参数格式为“PACKUSWB DEST, SRC”。
2.4 画图解释
用文字或伪代码来解释MMX指令都不太直观,用图片就直观多了。
在AMD手册(图4)中,就配有PACKUSWB指令的插图——
因为AMD手册严格根据参数顺序将mmx1(DEST)放在mmx2(SRC)的左边,造成该图的线条的发生交叉。交叉的箭头容易让人迷惑,但实际上各个数据还是连续存储的,并没有交叉存储(我以前也被它搞晕了,过了很久才明白过来)。
因此,我画了一张图片,更能清晰表示PACKUSWB指令的功能——
该图的风格与第3章(http://www.cnblogs.com/zyl910/archive/2012/03/27/noifopex3.html)的图片类似,左侧是内存中的源数据,右侧是运算结果,中间是MMX寄存器,箭头代表运算过程。该图 绘有三种操作——
1.加载(红色箭头)。将内存中的源数据(源缓冲区)加载到mmx寄存器。因为MMX寄存器是64位(8字节)的,所以该环节共加载了16字节数据,分别加载到2个mmx寄存器中(PACKUSWB需要两个操作数)。
2.运算(绿色将头)。这里就是PACKUSWB指令的功能,将 每个16位的带符号整数 饱和转换为 8位的无符号整数。
3.存储(蓝色箭头)。将mmx寄存器中的运算结果 存储到内存(目标缓冲区)。
观察该图会发现,PACKUSWB指令的功能正好与“将64位像素转为32位像素”的功能需求相吻合。所以我们可以利用PACKUSWB指令来实现“将64位像素转为32位像素”,并对比测试性能。
三、如何在VC中使用MMX指令集?
对于Visual C++ 6.0来说,依次打上SP5、PP5补丁后,就能支持MMX、SSE、SSE2这三套指令集。它们的下载地址是——
SP5(Visual Studio 6.0 Service Pack 5):http://www.microsoft.com/download/en/details.aspx?id=2618
PP5(Visual C++ 6.0 Processor Pack):http://msdn.microsoft.com/en-us/library/aa718349.aspx
对于更高版本Visual Studio,它们内置了对MMX指令集的支持,不需要安装补丁。详见——
http://www.cnblogs.com/zyl910/archive/2012/02/28/vs_intrin_table.html
Intrinsics头文件与SIMD指令集、Visual Studio版本对应表
3.1 使用内嵌汇编
在VC中,最直接的办法就是使用内嵌汇编,即利用“_asm”关键字直接写汇编语句。
例如下面那段代码,先尝试执行cpuid指令获得CPU特性,再尝试执行pxor指令测试系统中是否能运行MMX指令——
// 是否支持MMX指令集 BOOL simd_mmx() { const DWORD BIT_DX_MMX = 0x00800000; // bit 23 DWORD v_edx; // check processor support __try { _asm { mov eax, 1 cpuid mov v_edx, edx } } __except (EXCEPTION_EXECUTE_HANDLER) { return FALSE; } if ( v_edx & BIT_DX_MMX ) { // check OS support __try { _asm { pxor mm0, mm0 // executing any MMX instruction emms } return TRUE; } __except (EXCEPTION_EXECUTE_HANDLER) { } } return FALSE; }
http://www.cnblogs.com/zyl910/archive/2012/03/01/checksimd.html
[VC6] 检查MMX和SSE系列指令集的支持级别(最高SSE4.2)
3.2 使用Intrinsics函数
手工编写汇编代码是困难的、易错的、不易维护的。而且由于现代处理器强大的指令级并行性能,有些时候你辛辛苦苦编写的汇编代码,还没有编译器生成的代码速度快。
对于MMX指令来说,可以利用Intrinsics函数来避免手工编写汇编代码。
Intrinsics是对MMX、SSE等指令集的指令的一种封装,以函数的形式提供,使得程序员更容易编写和使用这些高级指令,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销
在MSDN中可以找到Intrinsics函数的帮助:http://msdn.microsoft.com/en-us/library/26td21ds(v=vs.110).aspx。具体的目录层次是——
MSDN Library
Development Tools and Languages
Visual Studio 11 Developer Preview
Visual Studio
Visual Studio Languages
Visual C++
Visual C++ Reference
C/C++ Languages
Compiler Intrinsics
Intrinsics函数的应用方法,推荐Alex Farber的《Introduction to MMX Programming》——
http://www.codeproject.com/Articles/4505/Introduction-to-MMX-Programming
Introduction to MMX Programming
中文翻译版见——
http://dev.gameres.com/Program/Other/mmxintro.htm
基于MMX指令集的程序设计简介
四、实际应用
现在我们想使用PACKUSWB指令,怎么知道该指令对应的Intrinsics函数呢?
最简单的办法就是查阅Intel手册。在Intel手册PACKUSWB指令的第三页(图3),列出Intrinsic函数的名称——
Intel C/C++ Compiler Intrinsic Equivalent PACKUSWB: __m64 _mm_packs_pu16(__m64 m1, __m64 m2) PACKUSWB: __m128i _mm_packus_epi16(__m128i m1, __m128i m2)
因现在是探讨MMX指令集,所以应该选用_mm_packs_pu16函数。
现在万事具备,可以完成MMX版的“将64位像素转为32位像素”函数了——
// 饱和处理MMX版 void f4_mmx(BYTE* pbufD, const signed short* pbufS, int cnt) { //const signed short* pS = pbufS; //BYTE* pD = pbufD; const __m64* pS = (const __m64*)pbufS; __m64* pD = (__m64*)pbufD; int i; for(i=0; i<cnt; i+=2) { // 同时对两个像素做饱和处理。即 将两个64位像素(4通道,每分量为带符号16位) 转为 两个32位像素(每分量为无符号8位)。 pD[0] = _mm_packs_pu16(pS[0], pS[1]); // 饱和方式数据打包(带符号16位->无符号8位)。等价于 for(i=0;i<4;++i){ pD[0].uB[i]=SU(pS[0].iW[i]); pD[0].uB[4+i]=SU(pS[1].iW[i]); } // next pS += 2; pD += 1; } // MMX状态置空 _mm_empty(); }
因PACKUSWB指令(_mm_packs_pu16)的功能,现在的内循环变得十分简单,一次就能处理2个像素。
注意这里将pbufS、pbufD这两个指针均设定为__m64指针类型。所以“pD += 1”实际上将指针地址前移了8个字节,而“pS += 2”将指针地址前移了16个字节。
最后不要忘了执行EMMS指令清除MMX状态,它对应的Intrinsics函数是_mm_empty。
五、改进测试方法
因MMX版的代码运行速度非常快,为了保证测试结果的精度,增加了“for(k=1; k<=nLoop; ++k)”这一层循环,所以在计算运行时间时除以了nLoop——
// 进行测试 void runTest(char* szname, TESTPROC proc) { const int nLoop = 16; // 使用MMX/SSE指令时速度太快了,只好再多循环几次 int i,j,k; DWORD tm0, tm1; // 存储时间 for(i=1; i<=3; ++i) // 多次测试 { //tm0 = GetTickCount(); tm0 = timeGetTime(); // main for(k=1; k<=nLoop; ++k) { for(j=1; j<=4000; ++j) // 重复运算几次延长时间,避免计时精度问题 { proc(bufD, bufS, DATASIZE); } } // show //tm1 = GetTickCount() - tm0; tm1 = timeGetTime() - tm0; printf("%s[%d]:\t%.1f\n", szname, i, (double)tm1/nLoop); // check //if (1==i) //{ // // 检查结果 // for(j=0; j<=16; ++j) // printf("[%d]:\t%d\t%u\n", j, bufS[j], bufD[j]); //} } }
在调用MMX版测试函数时,需要先检查是否支持MMX——
if (simd_mmx()) runTest("f4_mmx", f4_mmx);
六、测试结果
在32位winXP上的测试结果——
== noif:VC6 SIMD ==<Press any key to continue> f4_mmx[1]: 37.5 f4_mmx[2]: 37.3 f4_mmx[3]: 38.9
在64位win7上的测试结果——
== noif:VC6 SIMD ==<Press any key to continue> f4_mmx[1]: 37.1 f4_mmx[2]: 37.1 f4_mmx[3]: 36.1
硬件环境——
CPU:Intel Core i3-2310M, 2100 MHz
内存:DDR3-1066
参考文献——
《Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B & 2C): Instruction Set Reference, A-Z》. December 2011. http://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.html
《AMD64 Architecture Programmer's Manual Volume 5: 64-Bit Media and x87 Floating-Point Instructions》. December 2011. http://support.amd.com/us/Processor_TechDocs/26569_APM_v5.pdf
《INTEL 体系结构 MMX™ 技术开发者手册》。樊一鹏译。http://dev.gameres.com/Program/Other/Fmmx/index.htm
《PC平台新技术MMX 上册 ——开发编程指南》。吴乐南等著。东南大学出版社,1997年10月。
《Pentium Ⅱ/Ⅲ体系结构及扩展技术》。刘清森、马鸣锦、吴灏等著。国防工业出版社,2000年7月。
《编程高手箴言》。梁肇新著。电子工业出版社,2003年10月。
《Introduction to MMX Programming》。Alex Farber 著。http://www.codeproject.com/Articles/4505/Introduction-to-MMX-Programming
《基于MMX指令集的程序设计简介》(Introduction to MMX Programming)。?译。http://dev.gameres.com/Program/Other/mmxintro.htm
《在C/C++代码中使用SSE等指令集的指令(1)介绍》。gengshenghong著。http://blog.csdn.net/gengshenghong/article/details/7007100