zyl910

优化技巧、硬件体系、图像处理、图形学、游戏编程、国际化与文本信息处理。

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

  前面我们测试了高级语言做饱和处理的性能。其实,对于这样的大批量数据处理,使用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

posted on 2012-04-09 22:25  zyl910  阅读(3320)  评论(0编辑  收藏  举报