INTEL 体系结构 MMX 技术开发者手册

  整理制作: Yonsm.zj.com

INTEL 体系结构 MMX™ 技术开发者手册

目录

第一章 手册简介
  1.1 　关于本手册
  1.2 　参考文献
第二章 处理器体系结构和流水线简介
  2.1 　超标量(奔腾系列)的流水线和动态执行(P6系列)体系结构
    2.1.1 　　超标量(奔腾系列)的流水线
    2.1.2 　　动态执行(P6系列)流水线
  2.2 　高速缓存(Cache)
  2.3 　分支目标缓存
    2.3.1 　　相连分支
  2.4 写缓存Cache
第三章 MMX™代码开发规则
  3.1. 　规则列表及建议
    3.1.1 　　规则
    3.1.2 　　建议
  3.2 　常规的优化方案
    3.2.1 　　寻址方式
    3.2.2 　　对齐
      3.2.2.1 　　　代码
      3.2.2.2 　　　数据
    3.2.3 　　有前缀的操作码
    3.2.4 　　动态执行(P6系列)处理器中的部分寄存器阻塞
    3.2.5 　　有关分支预测的信息
      3.2.5.1 　　　动态分支预测
      3.2.5.2 　　　在动态执行(P6系列)处理器上的静态预测
  3.3 　调度
    3.3.1 　　通用配对规则
    3.3.2 　　整数配对规则
      3.3.2.1 　　　配对指令集
      3.3.2.2 　　　由于寄存器相关而不能配对的指令
      3.3.2.3 　　　特定配对
      3.3.2.4 　　　配对执行的限制
    3.3.3 　　MMX™指令配对准则
      3.3.3.1 　　　两个MMX™指令的配对
      3.3.3.2 　　　U管道的整数指令与V管道中的MMX™指令配对
      3.3.3.3 　　　U管道的MMX™指令与V管道中的整数指令配对
      3.3.3.4 　　　调度规则
  3.4 　指令选择
    3.4.1 　　使用访问内存的指令
    3.4.2 　　指令长度
  3.5 　高速缓存优化
    3.5.1 　　线读入顺序(命令)
    3.5.2 　　在高速缓存线中的数据对齐
    3.5.3 　　与分配效果
      3.5.3.1 　　　优化方法1: 布尔
      3.5.3.2 　　　优化方法2: 写前检查
  3.6 　内存优化
    3.6.1 　　对部分内部访问
    3.6.2 　　增加内存填充和视频填充的带宽
      3.6.2.1 　　　内存填充
      3.6.2.2 　　　视频填充
第四章 MMX™代码开发策略
  4.1 　制订计划
  4.2 　哪部分代码可使用MMX™技术改进
  4.3 　代码是浮点型还是整型
    4.3.1 　　混用浮点和MMX™代码
  4.4 　EMMS准则
  4.5 　CPUID的检测MMX™技术的用法
  4.6 　数据对齐
    4.6.1 　　堆栈对齐
  4.7 　数据安排
  4.8 　应用程序最后的调节
第五章 MMX™的编码技术
  5.1 　无符号分组
  5.2 　有符号分组
  5.3 　饱和模式下的交错成组
  5.4 　非饱和模式下的交错成组
  5.5 　非交错分组
  5.6 　含有一个常数的复数乘法
  5.7 　无符号数差值的绝对值
  5.8 　有符号数差值的绝对值
  5.9 　绝对值
  5.10 　有符号数截取到任意有符号区域[HIGH,LOW]
  5.11 　无符号数截取到任意无符号区域[HIGH,LOW]
  5.12 　常数生成
第六章 MMX™性能监测扩展
  6.1 　超标量(奔腾系列)性能监测事件
    6.1.1 　　MMX™指令事件说明
  6.2 　动态执行(P6系列)性能监测事件
  6.3 　RDPMC指令
    6.3.1 　　指令规则
附录A MMX™指令集
附录B MMX™命令和操作数限制


第一章 手册简介

INTEL 体系结构 MMX™ 技术开发者手册

第一章　手册简介

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

图1-1　新的数据类型

    另外，增加了八个新的64位MMX™寄存器，每个寄存器可按名称MM0-MM7直接访问。图1-2显示了这八个新的64位MMX™寄存器的布局。

图1-2　MMX™寄存器集

    MMX™技术对操作系统完全透明，并且与现有的、基于Intel体系结构的软件百分之百兼容。所有应用软件可继续在具有MMX™技术的处理器上正常运行。

    有关MMX™、数据类型和寄存器的其它信息和细节可详见《INTEL 体系结构 MMX™ 技术程序员参考手册》(Intel Architecture MMX™ Technology Developer's Manual)。

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

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

1.1　关于本手册
    本手册面向那些熟悉Intel体系结构的软件模型和汇编语言编程的读者。

    本手册描述了基于IA MMX™技术的软件编程的优化方案和使用策略。另外，它还包含了帮助你开始编写应用程序的编码技术和示例。

    本手册分为六章和一个附录：

    第一章——INTEL体系结构MMX™技术开发者手册介绍。

    第二章——处理器体系结构和流水线简介。本章对奔腾处理器和动态(P6系列)处理器的体系结构和流水线进行了简要介绍。

    第三章——MMX™代码开发原则。本章为帮助你开发出快速、高效的代码，提供了相关的原则和指导。另外，它还介绍了常规优化方法、指令调度和选择方案以及高速缓存和内存优化方法。

    第四章——MMX™代码开发策略。本章回顾了在应用程序中创建MMX™例程的步骤。

    第五章——MMX™的编码技术。本章的编码示例可以帮助你开始编写MMX™例程。

    第六章——MMX™性能监测计数器。本章详细说明了性能监测计数器和它们的功能。

    附录 A——MMX™指令集。附录汇总了全部MMX™指令。

1.2　参考文献
    参阅下列文档可获得有关Intel体系结构和本手册所涉及的专用技术的更多的内容：

《INTEL体系结构MMX™技术程序员参考手册》
Intel Architecture MMX™ Technology Programmers Reference Manual, Intel Corporation, Order Number 243007.

《奔腾系列处理器开发者手册》
Pentium® Processor Family Developer's Manual: Volume 1, 2, and 3, Intel Corporation, Order Number 241428, 241429, and 241430.

《高能奔腾处理器开发者手册》
Pentium® Pro Processor Family Developer's Manual: Volume 1, 2, and 3, Order Number 242690, 242691, and 242692.

《INTEL的32位处理器优化方案》
Optimizations for Intel's 32-bit Processors, Application Note AP-526, Order Number 242816


第二章 处理器体系结构和流水线简介

INTEL 体系结构 MMX™ 技术开发者手册

第二章　处理器体系结构和流水线简介

    本节简要介绍了具有MMX™技术的奔腾处理器和动态执行(P6系列)处理器的流水线及其结构特征。在了解了代码在处理器总线上的执行过程之后，你将会了解为什么这种优化方法能够提高代码的运行速度。另外，它将帮助你调度和优化你的应用程序，以获得更高的运行效率。

2.1　超标量(奔腾系列)的流水线和动态执行(P6系列)体系结构
2.1.1　超标量(奔腾系列)的流水线

    奔腾处理器是一个高级的超标量处理器。它是建筑在两个通用的整型流水线和一个可流水作业的浮点单元上的，这使处理器能够同时执行两条整型指令。一个对软件透明的动态分支预测机制能够使分支的流水线阻塞达到最小化。具有MMX™技术的奔腾处理器将为流水线增加新的处理阶段。MMX™流水线与整型流水线的集成方法非常相似。

    奔腾处理器可以在一个时钟周期内完成两条指令，一个流水线完成一条指令。第一个逻辑管道称之为“U”管道，第二个称之为“V”管道。在任何一条给定的指令译码期间，它安排的后面两条指令将被检查。并且，如果有可能，第一条指令被安排到“U”管道执行，第二条指令被安排到“V”管道执行。如果不能，则第一条指令被安排到“U”管道执行，“V”管道中不安排指令运行。

    指令在两个管道中运行与它们顺序执行所产生的效果是完全一样的。当发生管道阻塞时，后继的指令无法通过被阻塞的指令所在的任一管道中。图2-1说明了这种调度的流水线结构。

图2-1　MMX™流水线结构

    具有MMX™技术的奔腾处理器为整型流水线增加了一个额外的处理阶段。指令从代码的高速缓冲区中预取出来，被送入到“预取”(PF)阶段，并且在“提取”(F)阶段中进行指令的语法分析。另外，全部的前缀译码都在F阶段中进行。

    指令在先进先出(FIFO)的指令缓冲区中将语法分析与指令译码分开，这个缓冲区位于F阶段与译码1(D1)阶段之间。FIFO缓冲区的空间能够将被处理的指令上升到四条指令。FIFO缓冲区是透明的，当它为空时，不增加额外的迟延。

    在每个时钟周期内，可将两条指令压到指令的FIFO缓冲区中(根据有效的代码字节，以及其它因素，如前缀)。然后，再将成对的指令从FIFO缓冲区中弹出来，送到D1阶段中。由于指令的平均执行效率为每个时钟周期内不超过两条指令，所以FIFO通常是满的。只要FIFO是满的，就可以防止在指令提取和进行语法分析时产生的阻塞。如果发生了这样的阻塞，FIFO也可以使阻塞不在管道的执行阶段上发生。但如果FIFO空，由于流水线中无指令运行，则可能会导致一个执行阻塞。较长的指令或前缀可能会在FIFO入口处产生阻塞(参见3.2.3节和3.4.2节)。

    下面的图表详细说明了超标量处理器中的MMX™流水线以及流水线中发生阻塞的条件。

图2-2　在具有MMX™技术的奔腾处理器中的MMX™指令流

表2-1详细说明了每类MMX™指令的功能单元、迟延、吞吐量和执行管道。

表2-1　MMX™指令和执行单元

操作 | 功能单元个数 | 迟延 | 吞吐量 | 执行管道
ALU | 2 | 1 | 1 | U和V
乘法器 | 1 | 3 | 1 | U或V
移位/成组/分组 | 1 | 1 | 1 | U或V
内存访问 | 1 | 1 | 1 | U
整型寄存器访问 | 1 | 1 | 1 | U
  • 算术逻辑单元(ALU)用于执行算术和逻辑操作(即加、减、异或、与)。

  • 乘法器单元执行全部的乘法操作。乘法要求三个时钟周期，但通过流水线安排，可在每个时钟周期内完成一条乘法操作。处理器仅有一个乘法器单元，也就是说，不能同时运行两条乘法指令。但是，乘法指令可以同其它类型的指令配对而同时运行。它们可以在U管道或V管道中运行。

  • 移位单元执行全部的移位、成组或分组操作。由于同一时间只有一个移位器可以有效地进行移位、成组或分组操作，所以不能与其它移位单元的指令进行配对。但移位单元的指令可以和其它类型的指令配对运行。它们可以在U管道或V管道中运行。

  • MMX™指令只能在U管道中执行访问内存或整型寄存器，且不能与非MMX™指令配对运行。

  • 当对一个MMX™寄存器修改后，需经过两个时钟周期后，才能将MMX™寄存器移到内存或整型寄存器中。

    有关指令配对的信息请见3.3节。

    其它信息和指令格式请见《INTEL 体系结构 MMX™ 技术程序员参考手册》(Intel Architecture MMX™ Technology Developer's Manual)。

2.1.2　动态执行(P6系列)流水线

    P6系列处理器使用动态执行结构。该结构通过硬件寄存器重命名和分支预测的方法，将乱序执行和推测执行合成在一起。这些处理器有一个有序进入的流水线，它将Intel 386™的宏指令支解成简单的微操作(或UOP)和一个可以处理这些微操作的乱序的超标量处理器内核。这个乱序的处理器内核包含了几条流水线，连接了整型、跳转、浮点和内存执行单元。几种不同的执行单元可以集成在同一条流水线上。例如：一个整型地址逻辑单元和浮点执行单元(加法器、乘法器和除法器)同享一个流水线。数据高速缓冲区由一个专用的读取端口和其它的存储端口交错而成。大多数简单操作(整型ALU，浮点加法，甚至浮点乘法)可以按每时钟周期完成一至两个操作的吞吐量进行流水作业。浮点除法不可以进入流水线，长迟延操作可以和短迟延操作并行处理。

    P6系列的流水线由三部分构成：有序组织的前端(In-Order Issue Front-end)单元，乱序内核(Out-of-Order Core)单元和有序的退出(In-Order Retirement)单元。下面将详细介绍有序组织的前端单元。

图2-3　乱序内核与退出流水线

    由于动态执行处理器按乱序的方式执行指令，所以产生了数量充足的、可供执行的微操作，并使大多数有关性能调节方面的考虑得以实现。正确的分支预测和快速的译码是有序前端单元提高性能的核心。有关分支预测和分支目标缓冲区的详细内容见2.3节，下面讨论译码部分。

    在每个时钟周期内，ID1管道阶段可以对多达3条的Intel体系结构宏指令进行译码。但是，如果指令复杂或指令长度超过了7个字节，译码器的译码指令数将有所下降。

    译码器可以译码：

    (1) 每个时钟周期多达三条宏指令。
    (2) 每个时钟周期多达六条微操作。
    (3) 指令长度大于7的宏指令。

    P6系列处理器在D1阶段上有3个译码器。第一个译码器可在每个时钟周期完成一个由四个以下微操作构成的宏指令，其它两个译码器在每个时钟周期内完成一个由一个微操作构成的宏指令。由多于4个微操作构成的指令将耗费多个时钟周期来完成译码，在使用汇编语言编程时，按4-1-1微操作序列来安排指令将增加每个时钟周期内的译码指令数。通常：

  • 简单的寄存器——寄存器格式的指令仅用一个微操作。
  • 读取指令仅为一个微操作。
  • 存贮指令为两个微操作。
  • 简单的读-修改指令为两个微操作。
  • 简单的寄存器——内存格式的指令由2-3个微操作构成。
  • 简单的读-修改-写指令由4个微操作构成。
  • 复杂的指令通常超过4个微操作，故需耗费多个时钟周期译码。

    为了计算微操作，MMX™指令都是简单指令，见《Intel的32位处理器优化方案》(Optimizations for Intel's 32-bit Processors, Application Note AP-526, Order Number 242816)。附录D是一个说明Intel体系结构指令集中每一指令所用微操作数的表。

    一旦微操作被译码，它们从有序前端单元发送到保留(RS)站中，保留站是乱序核心管道阶段的开始部分。在RS中，微操作等待它们的操作数变为有效。一旦一条微操作的所有操作数有效，它将从RS中被送到一个执行单元。如果一个微操作在数据准备好的状态下进入RS(即为全部数据有效)，那么该微操作将立即被送到一个合适的且有效的执行的单元中。在这种情况下，在RS中的微操作只耗费了很少的时钟周期，全部的执行单元聚集在RS的输出端口上，一旦微操作执行完将返回到ROB，并等待退出。在这个管道阶段中，全部数据值被回写到内存并且全部的微操作按序退出，一次三条。下图提供了有关乱序核心和按序退出管道阶段的细节。

图2-4　乱序内核和退出流水线

表2-2　动态执行(P6-系列)处理器流水线执行单元

端口 | 执行单元 | 迟延/吞吐量
0 | 整型ALU单元 | 迟延1,吞吐量1/每周期
 | LEA指令 | 迟延1,吞吐量1/每周期
 | 移位指令 | 迟延1,吞吐量1/每周期
 | 整数乘法指令 | 迟延4,吞吐量1/每周期
 | 浮点单元 |
 | FADD指令 | 迟延3,吞吐量1/每周期
 | FMUL指令 | 迟延5,吞吐量1/2每周期1,2
 | FDIV单元 | 迟延长，数据多，不可流水作业的吞吐量
 | MMX™ALU单元 | 迟延1,吞吐量1/每周期
 | MMX™乘法单元 | 迟延3,吞吐量1/每周期
1 | 整型ALU单元 | 迟延1,吞吐量1/每周期
 | MMX™ALU单元 | 迟延1,吞吐量1/每周期
 | MMX™移位单元 | 迟延1,吞吐量1/每周期
2 | 取单元 | 命中高速缓存时,迟延3,吞吐量1/每周期4
3 | 存贮地址单元 | 迟延3,(非适用)吞吐量1/每周期3
4 | 存贮数据单元 | 迟延3,(非适用)吞吐量1/每周期

注释

    1.FMUL单元在接收第一个FMUL后，在下一时钟周期内不能接收第二个FMUL，但这并不是说只能在偶数时钟周期内执行FMUL。
    2.流水线上每两个时钟周期处理一个FMUL。可以视为P6系列处理器只有一个32x32->乘法流水线。
    3.存贮迟延对于数据流的全局来说并不十分重要。重要的是相应操作何时结束并退出。不同的存贮情况所产生的迟延也不同。例如，在时钟周期10，对一个部分地址进行数据存贮和地址存贮时，如地址100，在时钟周期10也可以对同样地址100读取(同样大小和类型)，且不被阻塞。
    4.对同一个地址读和写的指令可安排在同一时钟周期。

2.2　高速缓存(Cache)

    具有MMX™技术的处理器的在片高速缓存子系统，是由两个16K的4路线长为32字节的关联高速缓存体构成。高速缓存具有一个回写机制和一个伪LRU的置换算法。数据的高速缓存由八个按四字节边界交错的存贮体构成。

    在具有MMX™技术的奔腾处理器上，只要引用的数据不在同一个高速缓存体上，就可以被一条读取指令和一条存贮指令同时访问。在具有MMX™技术的奔腾处理器上，高速缓存访问失败的迟延为8个内部时钟周期。在具有MMX™技术的动态执行处理器中，最小迟延是10个内部时钟周期。

2.3　分支目标缓存

    具有MMX™技术的奔腾处理器和动态执行处理器在分支预测方面，除一个较小的异常处理外(本书2.3.1节中讨论)，在功能上完全一样。

    分支目标缓冲区(BTB)存贮了预先所见的分支和它们的目标。当一个分支被预取后，BTB将目标地址直接填入到指令读取单元(IFU)。一旦分支被执行，BTB将随着目标地址而改变。使用分支目标缓存时，预先所见的分支被动态预告。分支目标缓存的预测算法包括了模式匹配和每目标多达4位的预测历史位。例如，一个具有4个迭代长度的循环将百分之百地被正确预测到。遵循下列原则将提高预测性能：

    编写条件分支(除循环外)可将最常执行的分支紧接在分支指令后(即失败)。

    另外，具有MMX™技术的处理器有一个堆栈返回缓存(RSB, Return Stack Buffer)，可以连续地为不同地址上调用的过程正确地预测其返回地址，进一步为展开具有函数调用的循环带来了益处，并删除了某些需要in-line的过程。

2.3.1　相连分支

    另外，在具有MMX™技术的奔腾处理器上，如果两个分支指令的最后一个字节在同一个按四字节对齐的内存段内，则分支不可预测。如下图所示：

图2-5　相连分支的例子

    这种情况，发生在两个相连分支间没有间隔指令且第二个指令只有两个字节长的情况下(如+/-128字节的相对跳转指令)。

    为避免这种无法预测的情况，应使第二分支加长，在分支指令中用16位的相对位移代替8位的相对位移。

2.4　写缓存

    具有MMX™技术的处理器具有4个写缓存(相对无MMX™技术的奔腾处理器的两个写缓存)。另外，写缓存可以被U管道使用，也可以被V管道使用(相对无MMX™技术的奔腾处理器的一个写缓存对应一个管道的情况)。通过对内存写操作进行安排调度，可以提高关键循环的性能。如果你不想看到写未命中，每组指令不能安排多于4条写指令。并在安排另外的写指令前调度其它指令。


第三章 MMX 代码开发规则

INTEL 体系结构 MMX™ 技术开发者手册

第三章　MMX™代码开发规则

    下述规则将帮助快速地开发出高效的MMX™代码，并且这些代码可以在具有MMX™技术的所有的处理器上运行。

3.1　规则列表及建议

    以下给出一张规则和建议的列表。

3.1.1　规则

  • 使用当前的编译器可以产生一个优化的应用程序。这将帮助你从一开始能产生好的代码。
  • 避免部分寄存器的阻塞. 参见第3.2.4节。
  • 注意分支预测算法(参见第3.2.5节)。这是动态执行(P6-系列)处理器最为重要的优化方法。由于提高了分支预测能力，你的代码将耗费很少的周期来读取指令。
  • 对代码进行调度，使其达到最大的配对可能。参见第3.3节。
  • 确定所有的数据都已对齐。参见第4.6节。
  • 安排好代码，使其未命中的高速缓存指令最少，并优化预取。参见第3.5节。
  • 不要混用MMX™指令和浮点指令。参见第4.3.1节。
  • 避免使用除前缀为0F以外的操作码。参见第3.2.3节。
  • 对同一个内存区域，避免在大数据量存贮数据后进行小数据量读取。或小数据量存贮后进行大数据量的读取。对于同一个内存区域，读取和存贮数据应使用同样大小的数据量；并且地址要对齐。参见第3.6.1节。
  • 在任何时候，都要尽可能地使用OP，REG和MEM格式。这些格式可以帮助你减少占用寄存器，减少循环，避免产生不必要的读取操作。参见第3.4.1节。
  • EMMS必须放在MMX™指令的尾部。参见第4.4节。
  • 优化高速缓存中的数据的带宽，以符合MMX™寄存器的大小。参见第3.6节。

3.1.2　建议

  • 调整代码，一般情况下不使用向前条件分支，通常使用向后的条件分支。
  • 按16字节边界条件对齐频繁执行的分支目标。
  • 将循环展开来调度指令。
  • 使用软件方式来安排流水线以调度迟延和功能单元。
  • 必须成对使用CALL和 RET(return)指令。
  • 避免使用自修改代码。
  • 避免把数据放在代码段。
  • 尽可能快地计算出存贮地址。
  • 应避免使用包含三个或三个以上微操作代码或指令长度超过7个字节的指令。如果可能，使用只有一个微操作的指令。
  • 不要使用两个8位读取指令来进行16位的读取。
  • 在调用被调用保存(callee—save)过程前，先清除部分寄存器的内容。
  • 解决阻塞条件，如存贮地址，尽可能地避免可能引起阻塞的读取。
  • 一般情况下，一个可以直接由处理器支持的N-字节的数据(8位的字节，16值的字，32位的双字，32位、64位及80位浮点数)应该对齐在下一个最高的2的乘方边界处，避免未对齐的数据。

        ——按任意边界对齐8位数据。
        ——在已对齐的4-字节字数据内对齐16位数据。
        ——以4的任意倍数为边界，对齐32位数据。
        ——以8的任意倍数为边界，对齐64位数据。
        ——以128位为边界(即16字节的倍数)，对齐80位数据。

3.2　常规的优化方案

    本节概括了Intel体系结构的重要的常规优化技术。

3.2.1　寻址方式

    在奔腾处理器上，当一个寄存器被用作基地址元素时，如果该寄存器是前一个指令的目的寄存器(假设所有的指令都已在预取队列中)，将耗费一个附加的时钟周期，例如：

add esi, eax      ;esi是目的寄存器
mov eax, [esi]    ;esi是基地址，增耗费1个时钟

    因为奔腾处理器有两条整数流水线，如果一个寄存器是前一时钟内任意指令的目的寄存器，这个用于计算有效地址(在任一管道)的基地址或索引元素的寄存器，将耗费一个额外的时钟周期.这种效应称为地址生成互锁(AGI)。为了避免AGI，指令间应安排其它指令，并产生至少一个时钟周期的间隔来分隔这些指令。

    新增的MMX™寄存器不能当作索引寄存器或基地址使用，所以AGI不适用于MMX™寄存器为目的寄存器的情况。

    在 AGI情况下，动态执行(P6-系列)处理器不产生额外迟延。

    注意一些具有隐含寄存器读/写的指令，那些通过ESP(PUSH，POP，RET，CALL)而具有隐含的地址生成的指令，也会产生 AGI 附加迟延，如下例：

sub   esp, 24
;一个时钟周期的阻塞
push  ebx
mov   esp, ebp
;一个时钟周期的阻塞
pop   ebp

    PUSH和POP也对ESP进行隐含的写操作，但是如果下一条指令是通过ESP寻址时，将不产生 AGI。奔腾处理器通过PUSH和POP指令中的ESP“重命名”来避免AGI的额外迟延，如下例：

push  edi       ;无阻塞
mov   ebx, [esp]

    在具有MMX™技术的奔腾处理器上，包含立即数和偏移量的指令可在U管道中进行配对。如果有必要使用常数，那么使用立即数通常比把常数读取到寄存器更有效。但是如果同一个立即数被多次引用，应把常数先取到一个寄存器，然后多次使用这个寄存器，这种方法将更快一些，如下例所示：

mov  result, 555        ;555是一个立即数，result是偏移量
mov  word ptr[esp+4], 1 ;1是立即数，4是偏移量

    由于MMX™指令是双字节操作码(0x0F操作码映射)，任何一个使用基地址或使用具有4字节位移的索引寻址来访问内存的指令，其指令长度为8字节，超过7字节的指令一次只能部分译码，应尽量避免使用(见3.4.2节)。人们经常通过将立即数值加到基地址或索引寄存器的方法来减少这种指令的长度，因此要除去立即数部分。

    在Intel486™处理器中，当一个部分寄存器被写后，紧接着立即使用全部寄存器时，将产生一个附加的时钟周期。奔腾处理器在这方面无此后果，这种情况称之为部分阻塞条件，下例为奔腾处理器的例子。

mov  al, 0      ;1
mov  [ebp], eax ;2-在奔腾处理器上无迟延

    下列为Intel486™处理器对应的例子。

mov  al,0       ;1
;2-产生一个附加时钟周期
mov  [ebp],eax  ;3

    动态执行(P6-系列)处理器具有与Intel486™处理器一样的阻塞类型，甚至耗费更高。在对部分寄存器写操作结束前，读操作一直被阻塞。这样的耗费可能多于一个时钟周期。

    为达到最佳的性能，应避免在对部分寄存器(如AL，AH，AX)写操作后，使用包含这个部分寄存器的大寄存器。这条规则将防止动态执行处理器上的部分条件阻塞，并适用于所有大小寄存器对：

AL  AH  AX  EAX
BL  BH  BX  EBX
CL  CH  CX  ECX
DL  DH  DX  EDX
SP  ESP
BP  EBP
SI  ESI
DI  EDI

    有关部分寄存器阻塞的其它内容，请见第3.2.4节。

3.2.2　对齐

    本节提供了有关奔腾和动态执行(P6-系列)处理器在代码和数据对齐方面的信息。

3.2.2.1　代码

    奔腾和动态执行(P6-系列)处理器有一个32字节的高速缓存线。由于预取缓冲区按16字节的边界提取，代码的对齐对预取缓冲区的效率有直接影响。

    为使Intel体系结构系列处理器达到最佳性能，推荐如下方法：

  • 循环的人口标号应按下一个0 MOD l6对齐，当它与边界的距离小于8字节时。
  • 跟在条件转移后的标号不应被对齐。
  • 在无条件转移或函数调用后的标号应按下一个0 MOD l6 对齐，当它与边界的距离小于8字节时。

3.2.2.2　数据

    在奔腾处理器上, 对一个在高速缓存或总线上的未对齐数据进行访问，至少多耗费3个时钟周期。在动态执行(P6-系列)处理器上，对一个跨高速缓存线的末对齐数据进行访问，将耗费9-12个时钟周期。Intel推荐对数据按下述边界对齐，使全部处理器达以最佳执行性能。

2字节数据
    一个2字节的对象应完全包含在按4字节对齐的字内。(即，它的二进制地址应为XXXX00，XXXX01，XXXXl0，而不能是XXXX11)。

4字节数据
    4字节对象应按4字节边界对齐。

8字节数据
    一个8字节数据(64位，如双精度实数据类型，全部MMX™成组寄存器值)应按8字节边界对齐。

3.2.3　有前缀的操作码

    在奔腾处理器上，一个指令的前缀能够延缓语法分析并禁止指令配对。

    下表强调了FIFO中的指令前缀的影响。

  • 前缀为0F指令无附加迟延。
  • 前缀为66h，67h指令将花费一个时钟检测前缀，一个时钟计算指令长度和一个时钟进入FIFO(总共3个时钟)。它必须是第一条进入FIFO的指令，且另一条指令可与它一起被压入FIFO。
  • 具有其它前缀(非0F，66H，67H)的指令需要另一个附加时钟检测每一前缀。这些被压入FIFO的指令只能作为第一条指令。具有两个前缀的指令需用三个周期进入FIFO(两个时钟周期处理前缀，一个时钟周期处理指令)。另一个指令可以和第一条指令在同一时钟周期内一起压入FIFO。

    仅当FIFO保持两个以下人口时，才存在对性能的影响。只要译码器(D1阶段)有两条指令来译码，就没有额外开销。如果以每个时钟周期两条指令的频率，将FIFO中的指令撤出，FIF0就可迅速变空。所以，如果恰好位于一条有前缀的指令前的一些指令造成了性能损失(如，由高速缓存失误造成的阻塞引起不能配对、未对齐等等)，则造成有前缀的指令的性能损失可能被掩起来。

    在动态执行(P6-系列)处理器中，长度上超过7字节的指令将降低每时钟周期内译码指令数(见第2.1.2节)，前缀给指令增加了一到二个字节，可能使译码器受到限制。

    建议在任何时候都尽可能地不使用有前缀的指令，或将它们安排在因其它原因造成阻塞的指令后面。

    有关有前缀指令的配对详情，参见第3.3节。

3.2.4　动态执行(P6系列)处理器中的部分寄存器阻塞

    在动态执行(P6-系列)处理器中，当16或8位寄存器(如AL，AH，AX)被写后立即执行一个32位寄存器(如EAX)读操作，那么读操作被阻塞直到写结束(最少7个时钟周期)。考虑下面的例子，第一条指令移动数值8到AX寄存器，接下来的指令访问大寄存器EAX，这个代码导致一个部分寄存器阻塞。

MOV AX,   8       ←─┐
ADD ECX,  EAX   ←─┘ 发生在访问 EAX寄存器的部分阻塞

    对于全部8位和16位或32位寄存器时同样如此。

小寄存器 | 大寄存器
AL AH AX | EAX
BL BH BX | EBX
CL CH CX | ECX
DL DH DX | EDX

    奔腾处理器不存在这种附加迟延。

    由于P6系列的处理器可以按乱序执行代码。因此，这种相邻的指令不会产生阻塞。下例中也包含了一个部分阻塞。
MOV  AL, 8         ←──┐
MOV  EDX, 0x40             │
MOV  EDI, new-value      │
ADD  EDX, EAX       ←─┘发生在访问EAX寄存器的部分阻塞

    另外，任何跟在被阻塞的微操作后的微操作，也将等待被阻塞的微操作通过管道后，才能获得执行的时钟周期。通常为了避免阻塞，在对16位或8位小寄存器(AL)写操作后，不要对包含它的大寄存器进行读操作。

    为了使代码能够简便地应用于不同类型的处理器，在动态执行处理器中也存在读写小寄存器和大寄存器的特殊情况。下例的特殊情况中使用了XOR和SUB指令。

xor   eax, eax
movb  al,  mem8
use   eax         ←无部分阻塞
xor   eax, eax
movw  al,  meml6
use   eax         ←无部分阻塞
sub   ax,  ax
movb  al,  mem8
use   eax         ←无部分阻塞
sub   eax, eax
movb  al,  mem8
use   eax         ←无部分阻塞
xor   ah,  ah
movb  al,  mem8
use   eax         ←无部分阻塞

    通常，在实现这些指令序列时，总是在对寄存器写操作前，先对大寄存器清零。在这种特殊情况中，由XOR和SUB实现清零功能，且对EAX、EBX、ECX、EDX、EBP、ESP、EDI和ESI均有效。

3.2.5　有关分支预测的信息

    对动态执行(P6-系列)的处理器来说，分支优化是最重要的优化方案。这些优化方案也同样有益于奔腾处理器。

3.2.5.1　动态分支预测

    下列三个因素对动态分支预测是很重要的：

  1. 如果指令地址不在BTB中，预测结果为无分支的继续运行(失败)。
  2. 预测到的分支有一个时钟周期的迟延。
  3. BTB存贮了有关分支预测历史的4位数据。

    第一个因素建议将分支跟在将被执行的代码后面。决不要将数据跟在分支后面。

    为避免因提取分支而产生一个时钟周期的迟延，可简单地在这些分支之间加入一些额外工作。这个迟延限定了循环的最小耗费为两个时钟周期。如果你的小循环不超过两个时钟周期，就将它展开。

    分支预测器能够正确地对常规的分文模式进行预测。比如，它可以正确地预测出一个分支在循环中仅在每一奇次迭代时发生，而在每一偶次迭代中不发生。

3.2.5.2　在动态执行(P6系列)处理器上的静态预测

    在动态执行处理器中，对那些在BTB中没有历史数据的分支将使用静态预测算法进行预测，静态预测算法如下：

  • 预测无条件转移分支。
  • 预测向后的条件分支，本规则对循环适用。
  • 预测向前的条件分支。

    静态预测的额外开销为6个时钟周期。无预测或错误预测的额外开销大于12个时钟周期。下面的图表显示了静态预测的算法。

图3-2　动态执行(P6-系列)的静态分支预测算法

    下例说明了静态预测算法的基本规则。

A. Begin:
MOV   EAX, mem32
AND   EAX, EBX
IMUL  EAX, EDX
SHLD  EAX, 7
JC    Begin

    在这个例子中，在第一次通过时，向后分支不在BTB中，因此BTB未进行预测，但是静态预测器将预测到这个分支，所以不产生预测失误。

B.
MOV  EAX, mem32
AND  EAX, EBX
IMUL EAX, EDX
SHLD EAX, 7
JC   Begin
Begin:  Call Convert

    本代码段的第一个分支指令(JC Begin)是一个向前条件分支。第一次通过时，它不在BTB中，但静态预测器将预测到该分支。

    第一次通过时BTB发现Call Convert指令，但在BTB中没有进行预测。但静态预测算法能预测并提取一个该调用，这对一个无条件分支来说是正确的结果。

    这些例子中，条件分支只有两种情况：提取的和未提取的。间接分支，如开关语句，可计算的GOTO或通过指针的调用，都可跳到一个特定的地址单元上。如果分支有一个偏离的目标地址(即分支90%都转向同一地址)，那么BTB将在大多数情况能够精确预测到。但是如果目标地址是不可预测的，性能将迅速下降，用可预测的条件分支替换间接分支可以改善性能。

3.3　调度

    调度或配对是各代处理器都应使用的一种优化性能的方法。下面是一个在奔腾和P6系列处理器上，用以提高代码速度的配对和调度表。在某些情况下，对特定的处理器具有达到最佳性能的变通方案。这些变通的方法因应用程序的特性而不同。在超标量的奔腾处理器中，指令的顺序是使处理器达到最高性能的非常重要的因素。

    对指令顺序的重新安排增加了将两条指令安排成同时运行的可能性。与数据相关的指令应被至少一条其它指令所分开。

    本书描述了你需要了解的MMX™指令与整数指令配对的原则。对下表列出的每一种情况，都在小节中说明了与之相配的原则。

    进行配对所需了解的几个规则：

  • 通用配对规则：规则仅与机器状态有关，与指定的操作码无关。对整数和浮点同样有效。例如，单步指令禁止指令配对。
  • 整数配对规则：对整数指令配对的规则。
  • 对MMX™指令配对的规则：由于只存在一个乘法器单元，规则允许两个MMX™指令同时执行。
  • MMX™指令与整数指令配对规则：对一条MMX™指令和一条整数指令配对的规则。

注意　　浮点指令不可以与MMX™指令配对。

3.3.1　通用配对规则

    关于奔腾处理器通用配对规则，请参见《对Intel32位处理器的优化》应用部分 AP—526(序号242816)，具有MMX™技术的奔腾处理器降低了一些通用配对的规则要求：

  • 如果两条指令中存在一条长度大于7字节的指令，奔腾处理器不对它们进行配对。具有MMX™技术的奔腾处理器，不对第一条指令大于11字节或第二条指令大于7字节的指令进行配对，前缀不计算在内。
  • 有前缀的指令可在U管道配对，前缀为0FH，66H或67H的指令也可在V管道中配对。

3.3.2　整数配对规则

    当下列两种情况发生时不能进行配对：

  1. 后两条指令是不可配对指令(见下表可配对指令汇总：并参见《对Intel32位处理器的优化》应用部分AP-526，附录A包含了一个指令配对特性的完整列表)。通常，大多数简单的ALU指令是可配对的。
  2. 后两条指令属于寄存器争用(显式或隐式的)的一些类型。在较少的情况下，寄存器争用的指令也可配对。这种情况在3.3.2.3中解释。
表3-1　整数指令配对

在 U 管道中可配对指令 |  |  | 在 V 管道中可配对指令
mov r,r | alu r,i | push r | mov r,r | alu r,i | push r
mov r,m | alu m,i | push i | mov r,m | alu m,j | push l
mov m,r | alu eax,i | pop r | mov m,r | alu eax,i | pop r
mov r,i | alu m,r | nop | mov r,i | alu m,r | jmp near
mov m,i | alu r,m | shift/rot by l | mov m,i | alu r,m | jcc near
mov eax,m | inc/dec r | shift by imm | mov eax,m | inc/dec r | 0F jcc
mov m,eax | inc/dec m | test reg,r/m | mov m,eax | inc/dec m | call near
alu r,r | lea r,m | test acc,imm | alu r,r | lea r,m | nop
 |  |  |  | test reg,r/m | test acc,imm

3.3.2.1　配对指令集

    不可配对指令(NP)

  • 移位计数在cl的shift/rotate指令。
  • 长算术指令，如：MUL，DIV。
  • 扩充指令，如：RET，ENTER，PUSHA，MOVS，STOS，LOOPNZ。
  • 某些浮点指令，如FSCALE，FLDCW，FST。
  • 跨段(Inter-Segment)指令，如：PUSH sreg，CALL far。

    在 U或 V管道中的可配对指令(UV)

  • 大多数8/32位ALU操作，如：ADD，INC，XOR。
  • 所有的8/32位比较指令，如：CMP，TEST。
  • 所有8/32位对寄存器操作的指令，如：PUSH reg，POP reg。

    U管道中可配对指令(PU)

    下列指令必须在U管道中使用，并可与适当的V管道中指令配对，这些指令不能在V管道中执行。

  • 进位和借位指令，如：ADC，SBB。
  • 有前缀的指令，但0FH，66H或67H前缀的指令除外(参见3.2.3节)。
  • 用立即数移位的指令。
  • 某些浮点操作，如：FADD，FMUL，FLD。

    在V管道配对的指令(PV)

    这些指令可以在U管道或V管道中执行，但只能在V管道中配对。由于这些指令改变了指令指针(eip)，所以如果下一个指令可能是不相邻的指令时，不能在U管道中配对。即使当一个管道中的分支被预测为“未提取”时，当前指令也不能和下一指令配对。

  • 简单的控制传输指令，如：Call near，jmp near，jcc。包括jcc的短分支和jcc近分支(具有0F前缀)的条件分支指令。
  • fxch

3.3.2.2　由于寄存器相关而不能配对的指令

    指令配对也受指令操作数的影响，由于寄存器争用使下列组合不能配对，本规则的例外情况见下节。

  1. 第一条指令向一个寄存器写，而第二条指令从该寄存器中读(流相关)。例如：
    mov  eax, 8
mov  [ebp], eax
  2. 两条指令均写向同一寄存器(输出相关)，如下例：
    mov  eax, 8
mov  eax, [ebp]

    对EFLAGS寄存器写的指令配对不受此限(例如，两个变换状态编码的ALU操作)，配对后，指令执行的状态编码为V管道指令的状态。

    注意，两条指令中，如果第一条读一个寄存器，且第二条写一个已知状态(反相关)，则可以配对，见下例：

mov  eax, ebx
mov  ebx, [ebp]

    为了确定寄存器争用，对一个字节或一个字寄存器的引用视同对一个完整32位寄存器的引用。因此，

mov  al, 1
mov  ah, 0

    由于在EAX寄存器内容上输出相关，故不能配对。

3.3.2.3　特定配对

    某些特定指令不受上述“通用”规则限制。这些特定配对不受寄存器相关限制，且大多数隐式地读/写esp寄存器或隐式地写状态码。

    堆栈指针：

  • push reg/imm ; push reg/imm
  • push reg/imm ; call
  • pop reg ; pop reg

    状态代码：

  • cmp ; jcc
  • add ; jne

    注意这些特定配对由PUSH/POP指令指定，且只能是立即数或寄存器操作数，不能是内存操作数。

3.3.2.4　配对执行的限制

    有些配对指令可以同时安排进入管道，但却并不能并行执行。作为对U管道MMX™指令与V管道整数指令配对规则的补充，须增加下列两条规则。

  1. 如果两条指令都访问同一个数据高速缓存的存贮体，则第二条(V管道)指令的申请必须等到第一条指令的申请完成。当两个物理地址的第2至4位相同时，发生存贮体冲突。存贮体冲突的产生将对V管道的指令产生一个时钟周期的额外迟延。

  2. 跨管道的并发执行将保持内存访问的顺序。一条U管道中的多时钟周期的指令将单独执行，直到它最后的内存数据存取完成。
add  eax, meml
add  ebx, mem2  ; 1
(add)  (add)    ; 2  2-周期

    上述指令将寄存器内容和内存单元的数值相加，然后将结果置于寄存器中，带内存操作数的加法将耗费两个时钟周期。第一时钟周期从高速缓存中取数据，第二时钟周期执行加法，由于只有U管道指令有一个内存访问，V管道的加法可以在同一时钟周期启动。

add  mem1, eax      ; 1
(add)               ; 2
(add)add mem2, ebx  ; 3
(add)               ; 4
(add)               ; 5

    上述指令将寄存器内容与内存单元的数值相加，然后将结果置于内存单元中。结果放在内存的加法，执行时耗费三个时钟周期。第一个时钟周期取值，第二个时钟周期执行加法，第三个时钟周期存贮结果，在配对时，U管道指令的最后一时钟周期与V管道的第一时钟周期在执行时重叠。

    在已执行的指令未运行完时，其它指令不可运行。　　为了最佳地把握调度和配对的机会，一种较好的作法是：在耗费时钟周期数相等的情况下，尽量安排简单指令序列代替复杂指令。简单指令在组织时间片上占有优势。load/store类型的代码类要求更多寄存器并将增大代码大小。为满足这种对额外寄存器的需求，应在寄存器分配和指令调度上进一步努力，使仅当并行性扩大时，额外的寄存器可被使用。

3.3.3　MMX™指令配对准则

    本节说明了MMX™指令之间的配对准则和MMX™与整数指令的配对准则。

3.3.3.1　两个MMX™指令的配对

  • 由于只存在一个MMX™移位器单元，两条都使用MMX™移位器单元(pack，unpack和shift类指令)的MMX™指令不能配对。移位操作既可以安排在U管道执行，也可以安排在V管道执行，但不能在同一个时钟周期内安排在两个管道内执行。
  • 由于只存在一个MMX™乘法器单元，所以两条都使用MMX™乘法器单元(pmull，pmulh，pmadd类指令)的MMX™指令不能配对。乘法操作可以安排在U管道，也可以安排在V管道，但不能在同一时钟周期内安排在两个管道内执行。
  • 对内存或整数寄存器文件访问的MMX™指令只能安排在U管道中。不要将这些指令调度到V管道中。应使它们等待，并安排在下一个指令对中(且位于U管道中)。
  • U管道指令的MMX™目的寄存器不能与V管道指令的源或目的寄存器匹配(相关检查)。
  • EMMS指令不可配对。
  • 如果CR0.TS或CR0被设置，MMX™指令不能进入V管道。

3.3.3.2　U管道的整数指令与V管道中的MMX™指令配对

  • 浮点指令不能紧跟MMX™指令。
  • V管道的MMX™指令不能访问内存或整数寄存器文件。
  • U管道的整数指令是可配对的U管道整数指令(参见表3-1)。

3.3.3.3　U管道的MMX™指令与V管道中的整数指令配对

  • V管道指令是一个可配对的整数V管道指令(参见表3-1)。
  • U管道的MMX™指令不能访问内存或整数寄存器文件。

3.3.3.4　调度规则

    包括乘法指令在内，全部MMX™指令均可流水作业，除乘法指令耗费3个时钟周期外，全部指令执行时耗费一个时钟周期。

    由于乘法指令执行时用三个时钟周期，所以乘法指令后的结果只能被安排在三个时钟周期后的指令使用。考虑这一因素，避免在乘法指令后的两个指令对内调度一条与之相关的指令。

    如2.1.1节所述，在对寄存器写后再对寄存器存贮，必须等待两个时钟周期来修改寄存器。为避免管道阻塞，存贮调度在修改寄存器的两个时钟周期后进行。

3.4　指令选择

    下面描述了指令选择优化。

3.4.1　使用访问内存的指令

    一条MMX™指令可以具有两个寄存器操作数(“OP reg，reg”)或一个寄存器操作数及一个内存操作数(“OP reg，mem”)。在这里，OP代码指令操作数，reg代表寄存器，mem代表内存。“OP reg，mem”指令在某些情况下是有用的，因为它增加了每个时钟周期的操作数量，减轻了对寄存器需要的压力，并减少了代码长度。

    下列讨论假设内存操作数位于数据高速缓存中的情况。如果不是这样，附加迟延通常增大，使本节讨论的调度效果下降。

    在奔腾处理器中，“OP reg，mem”的MMX™指令的迟延不超过“OP reg，reg” 指令(假设能命中 Cache)。但这种配对机会较为有限(参见3.1节)。在动态执行(P6-系列)处理器中，“OP reg，mem”的MMX™指令，将被转换为两条微操作(与“OP reg，reg”指令转为一条微操作相比)。因此，它限制了译码的带宽(参见2.1.2节)，并比“OP reg，reg”指令占用更多的资源。

    推荐“OP reg，mem”指令的用法是依据MMX™代码是否受内存制约(即执行速度受内存访问限制)。由经验估计，如果下列不等式成立，则认为MMX™代码受内存制约。

指令数               非 MMX 指令数
------＜内存访问数 + -------------
2                       2

    对受内存制约的MMX™代码，Intel推荐对多次以上使用的同一内存地址的合并读取的方法，它将减少内存访问的次数。

    例：

    OP MM0, [address A]
    OP MM1, [address A]

    变成：

    MOVQ MM2, [address A]
    OP MM0, MM2
    OP MM1, MM2

    对不受内存制约的MMX™代码，仅在同一内存地址被使用两次以上时，推荐使用合并读取。在不能进行读取合并的地方，推荐使用“OP reg，mem”指令，使代码长度和指令数最少。

    例：

MOVQ  MM0, [address A]
OP    MM1, MM0

变成：

OP    MM1, [address A]

    在很多情况下，“MOVQ reg，reg”和“OP reg，mem”可被“MOVQ reg，mem”和“OP reg，reg”代替。由于它在动态执行处理器上节约一个微操作，因此在可以使用的地方尽量使用。

    例：(这里OP是对称操作)

MOVQ  MM1, MM0          (1个微操作)
OP    MM1, [address A]  (2个微操作)

变成：

MOVQ  MM1, [address A]  (1个微操作)
OP    MM1, MM0          (1个微操作)

3.4.2　指令长度

    对奔腾处理器来说，大于7字节的指令不能在V管道中执行。另外，只有当两条指令的长度都小于等于7字节时，才可都被压人指令的FIFO(见2.1.1节)。如果只有一条指令被压入FIFO，则不能进行配对，除非FIFO中至少已有一条指令。在配对可能性很高的代码中(在MMX™代码中常见)，或在末预测到分支后，FIFO可能为空，此时任何时候长度超过7字节的指令将导致配对失败。

    另外，当指令超过7字节时，动态执行(P6-系列)处理器一次只能对一条指令译码。

    因此，为在所有的Intel处理器上获得最佳性能，使用简单的长度不超过8字节的指令(参见3.4.1节　一种减少指令长度的方法)。

3.5　高速缓存优化

    高速缓存可以对你的应用程序的性能产生戏剧性的影响。在深刻理解高速缓存的工作原理后，你可对代码结构进行安排，最大限度地发挥高速缓存性能。有关高速缓存结构的内容，参见2.2节。

3.5.1　线读入顺序(命令)

    当对一个可高速缓冲的数据访问时，若该数据不在数据高速缓存中，将使整个高速缓存线从外部内存带入高速缓存，这就称之为线读入。对于奔腾或动态执行(P6-系列)处理器，这些数据按下列成组顺序以4个8字节段组成的成组读入。

第一个地址 | 第二个地址 | 第三个地址 | 第四个地址
0H | 8H | 10H | 18H
8H | 0H | 18H | 10H
10H | 18H | 0H | 8H
18H | 10H | 8H | 0H

    数据可以按它们到达的顺序有效地使用。如果一个数据组按串行顺序读出，那么可按串行顺序进行访问。因此，每个数据项可在它从内存到达时即被使用。

3.5.2　在高速缓存线中的数据对齐

    大小为32字节倍数的数组应在高速缓存线的起始位置。以32字节边界对齐，将充分发挥按序线读入的优势，并匹配高速缓存线大小。大小非32字节倍数的数组应以32或16字节边界开始(在高速缓存线的开始或中间)，为了按32或16边界对齐，需要对数据进行填充。如果必要，尽量按填充后的空间来定位数据(变量或常量)。

3.5.3　与分配效果

    动态执行(P6-系列)处理器有一个“按读分配写”的高速缓存，对应于奔腾处理器的“无写分配，通过写失败而写”的高速缓存。

    在动态执行(P6-系列)处理器中，写操作发生但被写部分不在高速缓存时，整个32字节高速缓存线被读入。在奔腾处理器中，当被写部分不在高速缓存时，仅简单地写到内存中去。

    由于连序存贮操作被合并为突发写，且将数据保存在高速缓存中可为后继的读取操作使用，使写分配通常是有利的，这就是动态执行(P6-系列)处理器采用这种写策略的原因，也是一些奔腾处理器的系统在设计L2 Cache时实现这种方式的原因。

    在以下情况下，写分配有如下缺点：

  • 仅对高速缓存线的一部分进行写操作。
  • 整个高速缓存线未读入。
  • 跨距超过32字节的高速缓存线。
  • 对大地址(>8000)写。

    当在一个应用程序中有大量的写操作，如下面例子所示，跨距大于32字节高速缓存线且数组为大数组时，对动态执行(P6-系列)处理器的每个写操作将使整个高速缓存线被读取。另外，这种读取将替换掉一条(有时两条)不用或很少用的高速缓存线。

    这样将导致每次存贮时增加一次对高速缓存线的读取，并降低程序的执行速度。当一个程序中有大量的写操作时，将使性能降低。Erastothenes筛选程序是一个说明这种高速缓冲效果的简单例子。在这个例子中，一个大数组不断地按增大的步长将其特定的值赋0。

注意　　这仅为一个表现高速缓存效果的简单例子。在代码中可使用很多其它的优化方法。

    Erastothenes筛选例子：

boolean array [2..max]

for (i=2; i<max; i++) {
    array:=1;
}

for (i=2; i<max; i++) {
    if (array[i]) {
        for(j=2; j<max; j+= i) {
            array[j]:=0;  /* 这里我们对内存赋0产生了j循环内对高速缓存线的读取*/
        }
    }
}

    对这个特定的例子来说, 有两种有效的优化方法。第一种是通过改用位数组来减少数组大小，目的是降低 Cache线的读取次数。每二种是通过检查前一次写的值，降低对内存的读写次数(波动较大高速缓存线)。

3.5.3.1　优化方法1: 布尔

    在上面的程序中，“Boolean”是一个字符型数组。在某些程序中，更好的方法是把“boolean”数组变成位数组，这样可以执行读——修改——写操作(因为高速缓存规程将每个读操作变成读——修改——写)。但在本例中，由于大多数的步长大于256比特(一个高速缓存线的位数)。故不能有效地提高性能。

3.5.3.2　优化方法2: 写前检查

    另一种优化方法是在写前检查该值是否已经是0。

boolean array[2..max]
for(i=2; i<max;i++)  {
    array:=1;
}

for(i=2; i<max; i++) {
    if(array[i]) {
        for (j=2; j<max; j+=i) {
            if(array[j]!=0) {   /* 检测该值是否已为0 */
                array[j]:=0;
            }
        }
    }
}

    由于在大多数时候筛选程序的数据已经是0，所以可以把对外部总线的驱动次数降低一半。

    通过预先检查，可以仅使用一个猝发总线周期读数据，并为每个不需要再写的缓存线节省一个猝发总线周期。由于对已修改的高速缓存线不再需要回写，可节省下额外的时钟周期。

注意 本操作仅对 P6-系列的处理器有意义，不能增加奔腾处理器的性能。因此，本操作不具备通用性。由于顺序存贮被合并为猝发写，高速缓存中的数据为下一次读取而保留，所以写分配在大多数系统中是一种通用的改善性能的方法，这也是为什么P6系列处理使用该策略，而一些基于奔腾处理器的系统在L2高速缓存中实现它的原因。

3.6　内存优化

3.6.1　对部分内部访问

    MMX™寄存器可以使你在不阻塞处理器的情况下移动大数据。对8位，16位，或32位长的单个数组值的读取，可考虑用对单个四字数据的读取来替代，然后增大对应的结构或数组的指针。

    任何被MMX™指令处理的数据必须按下列某一种方式读取：

  • 读取64位操作数的MMX™指令(如MOVP MM0，m64)，或
  • 对四字内存操作数处理的MMX™指令，具有寄存器——内存格式。指令(如PMAD DW MM0，m64)。

    全部的SIMD数据应由可存贮64位操作的MMX™指令存贮(如 MOVQ m64，MM0)。

    这些建议有两个目的：第一，使用四字数据块来读写SIMD数据更为效。第二，避免将对SIMD的8位，16位, 32位读写操作与MMX™对64位读写操作混淆。这些将防止下列两种情况发生，a)对同一内存区域先大数据量存贮然后小数据量读取；b)对同一内存区域先小数据量存贮，再大数据量读取。动态执行处理器将在上述情况下阻塞(参见3.1.1的规则表)。

    考虑下面的例子，在第一种情况中，在一系列小数据存贮后有一条对相同内存区的大读取(开始于内存地址“Mem”)，这个“大”读取将在这种情况被阻塞：

MOV  mem,   eax  ; 存贮双字到地址"mem"
MOV  mem+4, ebx  ; 存贮双字到地址"mem+4"
:
:
MOVQ mm0,  mem  ; 在地址"mem"取四字数据，阻塞

    MOVQ必须等待它所要求的数据全被写到内存后才执行。这种阻塞也发生在其它数据类型中(如，先以字节或字存贮，然后再对同一内存区按字或双字读)。如按下述所示改变代码顺序，在处理器访问数据时就不会发生迟延：

MOVD   mm1, ebx ;在将数据存贮到内存前，先将它变成四字数据
MOVD   mm2, eax
PSLLQ  mm1, 32
POR    mm1, mm2
MOVQ   mem, mm1 ; 按4字存贮SIMD变量到"mem"
:
:
MOVQ   mm0, mem ; 取四字SIMD变量"mem"，无阻塞

    在第二种情况中，在一个大存贮后有一系列对相同内存区的小读取(开始的内存地址“mem”)。这个“小”读将在这种情况下被阻塞.

MOVQ  mem, mm0  ; 在地址"mem"中存入四字数据
:
:
MOV   bx, mem+2 ; 在地址"mem+2"读取字数据，阻塞
MOV   cx, mem+4 ; 在地址"mem，4"读取字数据，阻塞

    对字数据的读取必须等待四字数据存贮完，使之可以访问所要求的数据。这种阻塞也发生在其它类型的数据上(如，先按双字或字存贮再按字或字节从同一内存地址中读取)。如按下述所示，改变代码顺序，在处理器访问数据时将不产生迟延。

MOVQ  mem, mm0  ; 在地址"mem"存入四字数据
:
:
MOVQ  mm1, mem  ; 在地址"mem"读取四字数据
MOVD  eax, mm1  ; 从MMX™寄存器中转移"mem+2"
; 非内存
PSRLQ mm1, 32
SHR   eax, 16
MOVQ  ebx, mm1  ; 从MMX™寄存器中转移"mem+4"
; 非内存
AND   ebx, 0fffh

    通常这些改变将为所要求的操作增加执行的指令数。对于动态执行(P6-系列)处理器来说，增加指令数所造成性能损失大于处理器本身的优越性。对奔腾处理器来说，由于上述改变不能带来好处，所以增加指令数对性能有消极影响。因此，小心有效地利用这些转移代码，能使对奔腾处理器性能的负面影响达到最小。

3.6.2　增加内存填充和视频填充的带宽

    了解内存是如何被访问或被填充是很有好处的。一个内存对内存的填充(如一个内存到视频的填充)被定义为：将32位(高速缓存线)数据从内存读取后，立即存贮回内存(如视频帧缓存)。下列准则可使顺序内存填充(视频填充)获得更高的带宽和更短的迟延。这些建议对所有的具有MMX™技术的Intel体系结构处理器有效，并适用于读/取操作未命中第二级高速缓存的情况。

3.6.2.1　内存填充

用MOVQ指令增加内存带宽

    对任何数值的读取，都将使整条高速缓存线的数据被调人到在片上高速缓存中，但用MOVQ代替32位存贮指令(如MOVD)向内存回存数据，将使每个内存填写周期减少一半的存贮次数。对某些基于奔腾处理器的系统，使用64位存贮指令代替32位存贮指令将提高30%带宽。另外，对于动态执行处理器，在同时具有用 MOVQ指令进行取和存的操作时，避免了对部分内存访问。

通过对同一DRAM页读取和存贮来增大内存带宽

    DRAM按页分开(不同于操作系统的页)。一个DRAM的页大小是由DRAM的大小和组成决定的。通常为几K字节。与操作系统的页相似，DRAM页也是顺序安排地址。对同一DRAM页的顺序内存访问，比对不同DRAM页的顺序访问迟延短。在很多系统中，页未命中(即两次访问不同页)的迟延是内存页命令迟延(两次访问同一页)的两倍。因此，如果内存填充周期中的读取和存贮用同一个DRAM页，就可以有效增加内存填充周期的带宽。

用对齐的存贮来增加内存填充的带宽

    未对齐的存贮使对内存的存贮次数增加—倍。Intel强调四字的存贮要按8字节对齐。四个对齐的四字数据的存贮，将导致一整条高速缓存线被写到内存。如果存贮的四字数据不是按8字节对齐的，那么每个MOVQ存贮指令产生两个32位写操作。在某些系统中，未对齐的64位存贮比对齐的64位数据的存贮低20%的带宽。

3.6.2.2　视频填充

使用64位存贮来增加视频的带宽

    尽管位于处理器与帧缓冲区之间的PCI总线宽为32位，但在大多数基于奔腾处理器的系统中，使用MOVQ存贮视频要比用两倍的32位数据存贮视频快. 这种现象的产生，是由于用四字数据存贮使 PCI写缓冲区的带宽增大造成的。

用对齐的数据存贮来增加视频的带宽

    当发生未对齐数据存贮时，将使视频带宽急剧下降。未对齐数据引起两倍的存贮。另外，对PCI总线(对帧缓冲区)的存贮迟延也将变长. 在 PCI总线上不可能发生淬发的未对齐数据存贮。在基于奔腾处理器的系统中，未对齐数据的视频填充带宽比对齐的视频填充带宽低80%。


第四章 MMX 代码开发策略

INTEL 体系结构 MMX™ 技术开发者手册

第四章　MMX™代码开发策略

    通常，开发IA处理器上的高速应用并不困难。如对体系结构具有深入了解且有很好的开发经验，则可使开发的应用程序充分发挥潜力，提高运行速度，使之成为一个高速的应用程序。具有MMX™技术的Intel体系结构的处理器为开发增加了新的研究方向。通过不断发展的转换技术可将性能有效地提高。为了开发MMX™代码，应测试当前的设备并选择最佳方式，来发挥MMX™指令的优势。如果你正在进行一项开发工作，作为开始，请现在就使用MMX™技术设计应用程序。

4.1　制订计划

    无论是修改现有应用程序还是创建新的应用程序，在使用MMX™指令来达到最佳性能时，需要考虑几个方面的因素。通常要先找到计算集中的代码段，且该代码段应是可修改为整数实现的，并有效支持高速缓存的体系结构。Intel性能工具箱提供了几个工具来辅助进行判断和调整。

    在进行这些工作前，应先回答下列问题：

  • 哪部分代码可用MMX™技术改进？
  • 使用MMX™技术是否可使当前算法达到最佳？
  • 代码是整型还是浮点型的？
  • 如何安排数据？
  • 代码是8位、16位还是32位的？
  • 应用程序是否需同时在有MMX™技术和无MMX™技术的处理器上运行？是否可用CPUID来创建一个可自适应的实现？

4.2　哪部分代码可使用MMX™技术改进？

    第一步　判断转换哪部分代码

    大多数应用程序都有几个计算高度集中的代码段。例如，语言压缩算法和过滤器，视频显示例程和绘制着色例程。这些例程通常较小，有重复的循环，对8位或16位整数操作，且占用了应用程序相当大的处理时间。这些例程使用MMX™技术优化库代码进行转换可获得最大的性能提高。将这些循环封装到优化的MMX™技术库，无论是否具有MMX™技术的平台，都将更灵活地给予支持。作为性能优化工具，类如Intel的VTune可视化调试工具，可用于把计算集中的代码分离出来。一旦将这些代码找到，将判断当前算法和修改后的代码哪一个具有最佳性能。在某些情况下，可能会改变算法中某些类型的操作来提高性能。用MMX™指令性能来匹配算法是达到最佳性能的关键。

4.3　代码是浮点型还是整型？

    第二步　判断算法包含的是浮点型数据还是整型数据

    如果当前算法是用整型数据实现的，则可简单判断一下算法中占用大多数微处理器时钟周期的代码部分在哪里。一旦判断出来，可用MMX™指令重新实现这些部分。

    如果算法中存在浮点型数据，则判断一下使用浮点型数据的原因。使用浮点操作通常有以下几个原因：性能、范围和精确度。如果性能因素是算法使用浮点数的原因，那么可考虑用MMX™整型代码替换算术来提高性能。

    如果数据范围或精确度的因素是算法使用浮点数用于进一步适应可能的范围或精确度的要求。可考虑是否能用整型数据达到所要求的范围和精确度？如果不能，代码最好仍用浮点代码。

4.3.1　混用浮点和MMX™代码

    如果使用MMX™代码，需要牢记的一点是八个MMX™寄存器是浮点寄存器的别名。由MMX™指令切换为浮点指令将增加多达五十个时钟周期，因此最好将这种切换次数减少到最小。不要将MMX™代码和浮点代码在指令级进行内部混用。如果一个应用程序频繁地进行MMX™代码和浮点代码间的切换，可考虑将之修改为只用MMX™指令流或浮点指令流来最大限度地降低由切换带来的额外迟延。

    当需混用浮点指令或MMX™指令编写应用程序时，下列准则可用来分离指令执行：

  • 将MMX™指令流和浮点指令流分为只包含单一类型代码的指令流。
  • 在转换中，寄存器内容无效。
  • MMX™代码结束时，使用EMMS指令置浮点标志字为空。
  • 浮点代码结束时，将堆栈置空。

    例如：

FP_Code:
...
...
...	/*离开时，将浮点栈置空*/

MMX™_Code:
...
...
...
EMMS	/*MMX™寄存器空*/

FP_Code1:
...
...
...
...	/*离开时，将浮点栈置空*/

    有关浮点编程模型的其它内容，请参见《奔腾处理器系列开发者手册》第三卷，体系结构和编程(序号241430)。

4.4　EMMS准则

    第三步　在MMX™代码结束时必须使用EMMS指令

    由于MMX™寄存器是浮点寄存器的别名，故一定要在使用浮点指令前，清空MMX™寄存器。使用EMMS指令清MMX™寄存器并置浮点标志字(TW)的值为空(即为全1)。该指令应在每个MMX™代码段的结束时插入，以避免执行浮点代码时产生浮点栈的溢出错误。

4.5　CPUID的检测MMX™技术的用法

    第四步　判断是否具有MMX™技术

    可用两种方法在应用程序中使用MMX™技术：第一种方法是在应用程序安装时检测是否具有MMX™技术。如果具有MMX™技术，则可安装相应的库代码。第二种方法是在程序执行时检测，并于运行时刻装人合适的库代码，这对于网络上的程序非常有效。

    为判断你的处理器是否具有MMX™技术，应用程序应检测Intel体系结构的特性标志。CPUID指令将特性标志返回到EDX寄存器中，程序可基于这一结果为系统选择合适的执行代码。

    特征标志字的第23位指示是否具有MMX™技术。当该位为1时，表示具有MMX™技术。下列代码段从特性标志字中提取值到EDX中，并检测是否具有MMX™技术。有关CPUID用法的其它内容请详见《用CPUID指令识别Intel处理器》应用程序部分AP-485，(序号241618)。

...			;CPUID指令识别是否具有MMX™
...			;识别Intel处理器
...
...
MOV	EAX, 1		;调用申请特性标志
CPUID			;0FH,A2H,CPUID指令
test	EDX, 00800000H	;在特性标志位(23位)=1时为MMX™技术存在
JNZ	Found

4.6　数据对齐

    第五步　确定数据己对齐

    很多编译器允许通过控制来指定对齐变量。通常可使变量按合适的边界对齐。但是，如果你发现有些变量未按指定边界取齐。清使用下列C语言写的算法对齐变量。这是按64位边界来对齐64位变量，一旦对齐后，对该变量的每次访问将节省3个时钟周期。

if(NULL==(new_ptr=malloc(new_value+1)*sizeof(var_struct))
mem_tmp=new_ptr;
mem_tmp/=8;
new_tmp_ptr=(var_struct*)((Mem_tmp+1)*8);

    另一种改进数据对齐的方法是将数据复制到己按64位边界对齐的地址中。当数据访问频繁时，可有效地提高性能。

4.6.1　堆栈对齐

    常规情况下，编译器对非静态的数据都分配到堆栈，而且通过存贮于堆栈的64位数据很方便地实现。如果可能，一定要使堆栈对齐。下列代码是使堆栈时齐的起始函数和结束函数。

Prologue:
push	ebp		;保存IH的帧指针
mov	ebp, sep		;生成新的帧指针
sub	ebp, 4		;建立栈指针的空iBq
and	ebp, 0FFFFFFC	;按64位对齐
mov	[ebp], esp	;保存旧的栈指针
mov	esp, ebp		;复制对齐的指针
sub	esp, FRAMESIZE	;分配空间
... callee	saves,	etc

epilogue:
... callee	restores,	etc
mov	esp, [ebp]
pop	ebp
ret

    对那些频繁访问而又无法对齐的数据，可将它们复制到已对齐的存贮空间去。

4.7　数据安排

    MMX™技术利用SIMD技术，来开发大多数多媒体算法中固有的并行机制。为了利用MMX™代码提高性能，内存中的数据应按下列准则进行格式化。

    请考虑一个将16位偏置数据加到一个向量的全部16位元素上的简单例子。在常规标量代码中，在循环开始时将偏置数据读入一个寄存器。在另一个寄存器中访问向量元素，并且一次只完成一条加法。

    将例程改换成MMX™代码后，由于MMX™指令可通过MOVQ指令一次处理四个向量元素，并通过PADDW指令一次完成四个加法。将使速度提高四倍。但是为了达到这一速度，在做加法时需要将偏置数据复制四份放在MMX™寄存器中。

    在原始的标量代码中，只有一个偏置数据位于内存。用MMX™指令可通过不同方法将四个偏置数据的备份放人MMX™寄存器中，或者通过预先对内存格式化，复制四个偏置数据。然后，只需在循环开始时，用MOVQ指令读取这些备份数据，就可使速度提高四倍。关于这种数据安排的另一个有趣的例子请详见第5.6节。

    由MMX™技术定义的新型64位成组数据类型可通过对未对齐数据进行优化而发挥更大能力。在使用MMX™指令和其它成组数据类型时，有很多算法。因为有的数据访问模式是未对齐的，如FIR过滤器这样一个简单的例子。F1R是一个按分支系数的数量长度产生有效的向量过滤器。如果过滤器对数据元素i的操作是一个从数据元素j开始产生的向量点(data[j]*coeff[0]+data[j+1]*coeff[1]+...+data[j+num_of_taps-1]*coeff[num_of_taps-1])，那么过滤器对数据元素i＋1的操作开始于数据元素j＋1。

    4.6节说明了64位内存数据对齐问题。假设你有一个已对齐的64位数据向量和一个已对齐的64位系数向量，则过滤器对第一个数据的操作是对齐的，但对第二个数据元素的操作时，每个对数据向量的访问将是未对齐的！对数据结构的复制和填充将避免算法中固有的访问未对齐数据的问题。在《MMX™指令计算一个16位实数FIR过滤器》应用部分#559(序号243044)中有一个说明如何避免FIR过滤器中未对齐问题的例子。

    注意复制和填充技术解决了未对齐问题，但要避免由于增大了数量，而造成未对齐数据访问的额外开销增大。在开发你的代码时，应考虑这种交换并通过选择开关来达到最佳性能。

4.8　应用程序最后的调节

    一旦应用程序的功能正确后，最佳的调节方法是使用一个分析器来检测在系统中运行的应用程序。Intel的VTune可视化微调工具就是这样一个工具，它要帮助你判断在你的应用程序中修改何处的代码可提高性能。另外，1nte1处理器提供了一个在片性能计数器，第6．1节中将说明这个计数器并解释如何使用它们。


第五章 MMX 的编码技术

INTEL 体系结构 MMX™ 技术开发者手册

第五章　MMX™的编码技术

    本节包括几个简单的例子来帮助你开始编写应用程序，最终目的是提供一些经常使用的、简单的低级操作。每个例子都使用最少的指令数来达到奔腾和P6系统处理器的最佳性能。

    每个例子包括：
  • 一个简短的描述
  • 核心代码
  • 必要的解释

    考虑到你可能会将这些例子插入到更长的代码序列中，故这些例子中不涉及有关调度问题。

5.1　无符号分组

    MMX™技术提供了几条用来将MMX™寄存器中的数据成组和分组的指令。分组指令可用于对一个无符号数进行零扩展(zero-extend)。下例假设源操作数是一个字组数据(16位)类型。

    输入：

    MM0:	源值
    MM7:	0

    如果需要，可用一个局部变量代替MM7寄存器。

    输出：

    MM0:	由2个低端字数据经零扩展形成的两个32位双字。
    MM1:	由2个高端字数据经零扩展形成的两个32位双字。

    MOVQ		MM1, MM0	;源数据复制
    PUNPCKLWD	MM0, MM7	;将两个低端字数据分组为两个32位双字
    PUNPCKHWD	MM1, MM7	;将两个高端字数据分组为两个32位双字

5.2　有符号分组

    当对一个分组时，应对有符号数进行符号扩展(sign-extend)。这一点与上述零扩展不同。下例假设源操作数是一个成组字数据(16位)类型。

    输入：

    MM0:	源值

    输出：

    MM0:	由2个低端字数据经符号扩展形成的两个32位双字。
    MM1:	由2个高端字数据经符号扩展形成的两个32位双字。

    PUNPCKHWD	MM1, MM0	;将源数据的两个高端字数据分组为
                ;目标数据的第2个和第4个字数据
    PUNPCKHLD	MM0, MM0	;将源数据的两个低端字数据分组为
                ;目标数据的第2个和第4个字数据
    PSRAD		MM0, 16	;将源数据的两个低端字数据
                ;经符号扩展形成的两个32位双字
    PSRAD		MM1, 16	;将源数据的两个高端字数据
                ;经符号扩展形成的两个32位双字

5.3　饱和模式下的交错成组

    PACK指令按预先定义的顺序将两个值合并到目的寄存器。特别说明的是，PACKSSDW指令将源操作数两个有符号双字和目的操作数的两个有符号双字合并成目的寄存器中的四个有符号字。如图5一1所示：

图5-1　PACKSSDW mm, mm/mm64指令示例

    下例将两个值交错送入目的寄存器，如图5一2所示。

图5-2　饱和模式下的交错成组示例

    本例中源操作数为有符号的双字，结果是交错的有符号字。成组指令不论是否是饱和模式都可执行。

    输入：

    MM0:	第一个有符号源值
    MM1:	第二个有符号源值

    输出：

    MM0:	第一、三字来自MM0中的饱和模式有符号双字，
        第二、四字来自MM1中的饱和模式有符号双字。

    PACKSSDW	MM0, MM0	;饱和模式有符号成组
    PACKSSDW	MM1, MM1	;饱和模式有符号成组
    PUNPKLWD 	MM0, MM1	;将操作数低端16位值交错

    成组指令总是假设源操作数为有符号数。目的寄存器中的结果由执行操作的成组指令定义。例如，PACKSSDW指令将两个源数据中各自的两个有符号32位值形成目的寄存器中的四个饱和的16位有符号值。另一方面，PACKUSWB指令将两个源数据中各自的四个有符号16位值形成目的寄存器中的四个饱和的8位无符号值。有关MMX™指令集的完整说明请见《Intel体系结构MMX™技术程序员参考手册》(Inte1 Architecture MMX™ Technology Programmers Reference Manual)，(序号243007)。

5.4　非饱和模式下的交错成组

    本例除结果字为非饱和的外，同上例相似。另外，为防止溢出，本操作只使用每个双字的低16位。

    输入：

MM0: 有符号源值
MM1: 有符号源值

    输出：

MM0: 第一、三字来自MM0中的双字的低16位，
第二、四字来自MM1中的的双字的低16位。

PSLLD MM1, 16 ;将每个双字值中的LSB移16位
;到MSB位置
PAND MM0, {0, FFFF, 0, FFFF}
;将每个双字值的16MSB标记为0
POR MM0, MM1 ;合并两个操作数

5.5　非交错分组

    分组指令将目的操作数和源操作数的数据元素交错合并到目的寄存器中。下例非交错地将两个操作数合并到目的寄存器中。例如，从源数据1中的字组数据类型中取两个相邻元素，并将该值放到结果的低32位中。然后从源数据2中的字组数据类型中取两个相邻元素，并将该值放到结果的高32位中。目的寄存器的一种组合形式如图5一3所示。

图5-3　MM0中的非交错分组的结果

    目的寄存器的另一种相反的组合如图5一4所示。

图5-4　MM1中的非交错分组的结果

    下例将两个字组的源数据按非交错方式分组。其实现方法为用将双字变为四字的指令代替将字变为双字的分组指令。

    输入：

    MM0:	字组源值
    MM1:	字组源值

    输出：

    MM0:	包含两个非交错的初始源值的低端字
    MM2:	包含两个非交错的初始源值的高端字

    MOVQ		MM2, MM0	;复制源值1
    PUNPCKLDQ	MM0, MM1	;用MMl的两个低端字替换MM0的两个高端字，
                ;保留MM0中的两个低端字
    PUNPCKHDQ	MM2, MM1	;将MM2的两个高端字移到低端，
                ;将MM1的两个高端字送入MM2的两个高端字

5.6　含有一个常数的复数乘法

    复数的乘法由四个乘法和两个加法构成。PMADDWD正是这样的指令。为了使用该指令，仅需将数据格式化为四个16位值。每个实部和虚部值应为16位值。

    令输入数据为Dr和Di

        这里　Dr=数据的实部
              Di=数据的虚部

    将复数的常系数格式化为内存中的四个16位值[Cr -Ci Ci Cr]。

    记住使用MOVQ指令将值取出送到MMX™寄存器中。

    输入：

MM0: 由Dr,Di构成的复数
MM1: 在格式[Cr -Ci Ci Cr]中的常复数系数

    输出：

MM0: 包含[Pr Pi]的两个32位双字复数，
其实部是由Pr=Dr*Cr-Di*Ci生成，
复数的虚部是由Pi=Dr*Ci+Di*Cr生成。

PUNPCKLDQ MM0，MM0 ;生成［Dr Di Dr Di］
PMADDWD MM0，MM1 ;结果为[(Dr*Cr-Di*Ci)(Dr*Ci+Di*Cr)]

    注意，输出为一个字组。如果需要，可用一条成组指令将结果转换为16位值(与输入格式匹配)。

5.7　无符号数差值的绝对值

    本例计算两个无符号数差值的绝对值。假设是一个无符号字节组数据类型。这里，我们使用无符号饱和模式下的减法指令。指令按无符号饱和方式将无符号操作数相减。仅支持字节组和字组，不支持双字组。

    输入：

MM0: 源操作数
MM1: 源操作数

    输出：

MM0: 无符号数差值的绝对值

MOVQ MM2, MM0 ;制做MMO的备份
PSUBSB MM0, MM1 ;计算差值方法1
PSUBSB MM1, MM2 ;计算差值方法2
POR MM0, MM1 ;将两个差值求或

    如果操作数有符号，本例不执行。有关有符号差值的绝对值请见下一个例子。

5.8　有符号数差值的绝对值

    本例计算两个有符号数差值的绝对值。MMX™指令中没有按无符号饱和方式对有符号操作数进行减法的操作。这里使用的技巧是，先将两个输入的操作数的对应元素按大小排序，生成一个较大的字组数据和一个较小的字组数据。然后，用较大的值减去较小的值，就生成了所要求的差值的绝对值。关键是由B=XOR(A，XOR(A,B))和A=XOR(A,0)构成的快速排序技术。然后在成组数据类型中，使某些元素为XOR(A,B)，某些为0，你可以将这一操作数与A进行XOR操作，并在某些地方生成A,某些地方生成B。下例假设一个字组数据类型，每个元素为一个有符号数值。

    输入：

MM0: 有符号源操作数
MM1: 有符号源操作数

    输出：

MM0: 有符号数的差值的绝对值

MOVQ MM2, MM0 ;制做源操作数1的备份(A)
PCMPGTW MM0, MM1 ;取得源操作数1>源操作数2的标志
MOVQ MM4, MM2 ;制做源操作数A的另一个备份
PXOR MM2, MM1 ;生成交换操作XOR(A,B)的中间值
PAND MM2, MM0 ;生成0s和XOR(A,B)元素的标志
;当A>B为值XOR(A,B)，当A<=B为值0
MOVQ MM3, MM2 ;生成交换掩码的备份
PXOR MM4, MM2 ;XOR(A,swap mask)为较小值
PXOR MM1, MM3 ;XOR(B,swap mask)为较大值
PSUBW MM1, MM4 ;差值的绝对值=较大值-较小值

5.9　绝对值

    当X为有符号数时，计算|X|。下例假设操作数为有符号字。

    输入：

MM0: 有符号源操作数

    输出：

MM1: ABS(MM0)

MOVQ MM1, MM0 ;备份X
PSRAW MM0, 15 ;复制符号位(是双字则用31位)
PXOR MM0, MM1 ;如负数则取1的补码
PSUBS MM1, MM0 ;如为负数则加1

    注意　最大的负数(即16位的8000H)所求的绝对值不正确，但该代码对这种情况也是合理的，它使减一的结果为7fffH。

5.10　有符号数截取到任意有符号区域[HIGH,LOW]

    本例说明了如何将一个有符号值截取到有符号区域[HIGH,LOW]。特别是当值小于LOW或大于HIGH时，截取到相应的LOW或HIGH中。本技术按无符号饱和方式进行成组数据加和成组数据减指令，该技术只能用于字节组和字组数据类型。

    下例使用了最大成组数和最小成组数。

    下例显示了对字数据值的操作。为简便起见，使用下列常数(在对字节值操作时，使用相应的常数)。

  • PACKED_MAX=0x7FFF7FFF7FFF7FFF
  • PACKED_MIN=0x8000800080008000
  • PACKED_LOW包含了由LOW值构成的、字组数据类型中的全部4个字。
  • PACKED_HIGH包含了由HIGH值构成的、字组数据类型中的全部4个字。
  • PACKED_USMAX为全1。
  • HIGH_US将HIGH值加到PACKED_MIN中的全部元素上。
  • LOW_US将LOW值加到PACKED_MIN中的全部元素上。

    本例显示了对字数据值的操作。

    输入：

MM0: 有符号源操作数

    输出：

MM0: 按无符号区域[HIGH,LOW]截取的有符号操作数

PADD MM0, PACKED_MIN ;非饱和方式相加并转换为无符号数
PADDUSW MM0, (PACKED_USMAX-HIGH_US)
;截取HIGH
PSUBUSW MM0, (PACKED_USMAX-HIGH_US+L0W_US)
;截取到LOW
PADDW MM0, PACKED_LOW ;取消前两个偏移量

    上述代码先将值转换为无符号数，然后将它们截取到一个无符号区域。最后一条指令将数据转换为有符号数据，并置于有符号区域中。当数据(HIGH-LOW)<0x8000时，将数据转换为无符号数的结果是正确的。

    如果(HIGH-LOW)>=0x8000，算法应按下面所示进行简单修改。

    MM0:	有符号源操作数

    输出：

    MM0:	按无符号区域[HIGH,LOW]截取的有符号操作数

    PADDSSW	MM0, (PACKED_USMAX-HIGH_US)	;截取HIGH
    PSUBSSW	MM0, (PACKED_USMAX-PACKED_HIGH+PACKED_LOW)
                        ;截取到LOW
    PADDW	MM0, LOW				;取消前两个偏移量

    如果已知(HIGH-LOW)>=0x8000，本算法则省下一个时钟周期。为了了解为什么在(HIGH-LOW)<0x8000时，这个由三条指令构成的算法不能工作，注意0xFFFF减任何小于0x8000的数都将产生一个在数量级上大于负数0x8000的数。当PSUBSSW MM0, (0xFFFF-HIGH+LOW)(三行算法的第二条指令)被执行时，一个负数将被减，造成MM0中的值不减反加，这样就产生了一个不正确的结果。

5.11　无符号数截取到任意无符号区域[HIGH,LOW]

    本例说明了如何将一个无符号数截取到无符号区域[HIGH,LOW]。如果数据小于LOW或者大于HIGH，那么截取到对应的LOW或HIGH中。本技术按无符号饱和方式进行成组数据加和成组数据减指令，该技术只能用于字节组和字组数据类型。

    下例显示了对字数据值的操作。

    输入：

MM0: 无符号源操作数

    输出：

MM0: 按无符号区域[HIGH,LOW]截取的无符号操作数

PADDUSW MM0, 0xFFFF-HIGH ;截取到HIGH
PSUBUSW MM0, (0xFFFF-HIGH+LOW) ;截取到LOW
PADDW MM0, LOW ;取消前两个偏移量

5.12　常数生成

    在MMX™指令集中不存在能够将立即数常数取到MMX™寄存器的指令。下列代码段用于生成MMX™寄存器中的常用常数。当然，也可以将常数作为内存中的局域变量。但是如果要这样做，就要在内存中进行数据的复制，以便可用MOVQ指令读取。

    在MM0中产生0：

PXOR MM0, MM0

    在MM1中产生全1数，每个成组数据类型字段为-1

PCMPEQ MM1, MM1

    为每个字节组[或成组字](或双字组)字段产生常数1

PXOR MM0, MM0
PCMPEQ MM1, MM1
PSUBB MM0, MM1　[PSUBW　　MM0, MM1]　(PSUBD　　MM0, MM1)

    在每个字组(或双字组)中生成有符号常数2n-1

PCMPEO MM1, MM1
PSRLW MM1, 16-n　(PSRLD　　MM1, 32-n)

    在每个字组（或双字组）字段中生成有符号常数-2n

PCMPEO MM1, MM1
PSLLW MM1, n　　(PSLLD　　MM1, n)

    由于MMX™指令集不支持字节的移位指令，2n-1和-2n只与字组和双字组相关。


第六章 MMX 性能监测扩展

INTEL 体系结构 MMX™ 技术开发者手册

第六章　MMX™性能监测扩展

    对于改善代码性能来说，最有效的方法是找到性能瓶颈。Intel体系结构的处理器上具有一个计数器，可以用来收集有关应用程序的性能情况。该计数器在代码运行时跟踪所产生的事件。你可以在代码运行时或因阻塞而终止时，读出计数器值。关于这一点，可以通过使用Intel的VTune预处理工具或在代码中使用某些指令来实现。

    本节描述了具有MMX™技术的奔腾处理器和P6系列处理器对MMX™代码的性能监测方面的特性。

    RDPMC指令详见6.3节。

6.1　超标量(奔腾系列)性能监测事件

    为支持MMX™技术，在所有的奔腾处理器上增加了特性性能计数器和几个新的事件。全部的新事件被分配到两个事件计数器(CTR0，CTR1)之一，并按“双事件”异常(类如“D1空闲”和“FIFO空”)被分配到不同计数器以允许并行测试。事件必须被分配到指定的计数器。表6-1列出了性能监测事件。新增事件也被列出。

表6-1　性能监测事件

序号 | 编码 | 计数器0 | 计数器1 | 性能监测事件 | 突发或持续
0 | 000000 | 有 | 有 | 数据读 | 突发
1 | 000001 | 有 | 有 | 数据写 | 突发
2 | 000010 | 有 | 有 | 数据TLB未命中 | 突发
3 | 000011 | 有 | 有 | 数据读未命中 | 突发
4 | 000100 | 有 | 有 | 数据写未命中 | 突发
5 | 000101 | 有 | 有 | 对M或E状态行写(命中) | 突发
6 | 000110 | 有 | 有 | 回写数据高速缓冲行组 | 突发
7 | 000111 | 有 | 有 | 窥探外部数据高速线 | 突发
8 | 001000 | 有 | 有 | 窥探命中外部数据高速缓存 | 突发
9 | 001001 | 有 | 有 | 双管道内存访问 | 突发
10 | 001010 | 有 | 有 | 存贮体冲突 | 突发
11 | 001011 | 有 | 有 | 未对齐内存数据或I/O引用 | 突发
12 | 001100 | 有 | 有 | 读代码 | 突发
13 | 001101 | 有 | 有 | 未命中TLB代码 | 突发
14 | 001110 | 有 | 有 | 未命中代码高速缓存 | 突发
15 | 001111 | 有 | 有 | 任一段寄存器被读取 | 突发
16 | 010000 | 有 | 有 | 保留 |
17 | 010001 | 有 | 有 | 保留 |
18 | 010010 | 有 | 有 | 分支 | 突发
19 | 010011 | 有 | 有 | BTB预测 | 突发
20 | 010100 | 有 | 有 | 分支发生或BTB命中 | 突发
21 | 010101 | 有 | 有 | 流水线刷新 | 突发
22 | 010110 | 有 | 有 | 已执行指令 | 突发
23 | 010111 | 有 | 有 | 在V管道执行的指令并行/配对 | 突发
24 | 011000 | 有 | 有 | 总线循环处理的时钟 | 持续
25 | 011001 | 有 | 有 | 由于写缓冲满造成的阻塞时钟数 | 持续
26 | 011010 | 有 | 有 | 由于等待读内存数据造成的管道阻塞 | 持续
27 | 011011 | 有 | 有 | 对E或M状态行写时的阻塞 | 持续
29 | 011101 | 有 | 有 | I/O读或写循环 | 突发
30 | 011110 | 有 | 有 | 读非高速缓存数据 | 突发
31 | 011111 | 有 | 有 | 由于地址生成互锁造成的管道阻塞 | 持续
32 | 100000 | 有 | 有 | 保留 |
33 | 100001 | 有 | 有 | 保留 |
34 | 100010 | 有 | 有 | FLOPs | 突发
35 | 100011 | 有 | 有 | 在DR0寄存器的断点匹配 | 突发
36 | 100100 | 有 | 有 | 在DR1寄存器的断点匹配 | 突发
37 | 100101 | 有 | 有 | 在DR2寄存器的断点匹配 | 突发
38 | 100110 | 有 | 有 | 在DR3寄存器的断点匹配 | 突发
39 | 100111 | 有 | 有 | 硬件中断 | 突发
40 | 101000 | 有 | 有 | 数据读或数据写 | 突发
41 | 101001 | 有 | 有 | 未命中的数据读或写 | 突发
43 | 101011 | 有 | 无 | U管道中执行的MMX™指令 | 突发
43 | 101011 | 无 | 有 | V管道中执行的MMX™指令 | 突发
45 | 101101 | 有 | 无 | 执行的EMMS指令数 | 突发
45 | 101101 | 无 | 有 | MMX™指令与FP指令切换次数 | 突发
46 | 101110 | 无 | 有 | 对非高速缓存内存写 | 突发
47 | 101111 | 有 | 无 | 执行的饱和模式MMX™指令 | 突发
47 | 101111 | 无 | 有 | 执行饱和模式 | 突发
48 | 110000 | 有 | 无 | 非HLT状态下的循环次数 | 持续
49 | 110001 | 有 | 无 | 读数据的MMX™指令 | 突发
50 | 110010 | 有 | 无 | 浮点阻塞 | 持续
50 | 110010 | 无 | 有 | 分支发生 | 突发
51 | 110011 | 无 | 有 | D1空闲且在FIFO中有一条指令 | 突发
52 | 110100 | 有 | 无 | 写数据的MMX™指令 | 突发
52 | 110100 | 无 | 有 | 未命中的写数据MMX™指令 | 突发
53 | 110101 | 有 | 无 | 由分支预测错造成的管道刷新 | 突发
53 | 110101 | 无 | 有 | 由于在WB阶段的分支预测错造成流水线刷新 | 突发
54 | 110110 | 有 | 无 | MMX™指令对未对齐内存数据的引用 | 突发
54 | 110110 | 无 | 有 | 等待MMX™指令读内存数据的流水线阻塞 | 持续
55 | 110111 | 有 | 无 | 不正确的返回预测 | 突发
55 | 110111 | 无 | 有 | 预测的返回 | 突发
56 | 111000 | 有 | 无 | MMX™指令乘法单元互锁 | 持续
56 | 111000 | 无 | 有 | 由前一个操作造成的MOVD/MOVQ存贮阻塞 | 持续
57 | 111001 | 有 | 无 | 返回数 | 突发
57 | 111001 | 无 | 有 | RSB溢出 | 突发
58 | 111010 | 有 | 无 | BTB入口错误 | 突发
58 | 111010 | 无 | 有 | BTB对未接受的分支未预测 | 突发
59 | 111011 | 有 | 无 | 在执行MMX™指令时由于写缓冲区满而造成的阻塞时钟数 | 持续
59 | 111011 | 无 | 有 | MMX™指令对E或M行写阻塞 | 持续

6.1.1　MMX™指令事件说明

    事件的编码/计数值在园括号中。

  • 在U管道中执行的指令(101011/0)：
        ——在U管道中执行的指令总数。
  • 在V管道中执行的指令(101011/1)：
        ——在V管道中执行的指令总数。
  • 执行的EMMS指令(101101/0)：
        ——执行的EMMS指令总数。
  • 在MMX™指令与FP指令间切换次数(101101/1)：
        ——对第一条跟在浮点指令后的MMX™指令或第一条跟在MMX™指令后浮点指令计数。可用来估算在浮点和MMX™状态间切换的额外开销。偶数值说明处理器处于MMX™状态，奇数值表示处理器处于浮点状态。
  • 对非高速缓存的内存写(101110/1)：
        ——对不可进行高速缓存的内存进行写操作的次数，它包括了由TLB未命中所造成的写周期和I/O写周期。按未被重计数的BOFF#重开始的周期。
  • 按饱和方式执行的MMX™指令(101111/0)：
        ——饱和方式执行的MMX™指令所计总数。不管它们是否真的以饱和方式处理。饱和方式的MMX™指令可以执行加法、减法或者成组操作。
  • 饱和执行(101111/1)：
        ——为至少有一个结果为实际的饱和处理的饱和算术结果的MMX™指令个数计数(即，如果一个MMX™指令对四个饱和双字中的三个进行操作，而计数器仅加一)。
  • 不处于停机(HLT)状态的周期数(110000/0)：
        ——本事件为处理器中由停机HALT(HLT)指令造成的非空闲的周期数计数。该事件将允许用户计算“净CPI”。注意，在处理器正在执行HLT指令的时间内，时间标记计数器(TSC)被禁止。由于本事件被计数控制器CC0控制，故CC1可用于计算在CPL=3时TSC不能提供的CPI数(注：CPI为每条指令所用的周期次)。
  • 读数据的MMX™指令(110001/0)：
        ——类似“数据读”，只计算MMX™指令的访问次数。
  • 未命中的MMX™读数据指令(110001/1)：
        ——类似“读数据未命中”，只计算MMX™指令访问次数。
  • 浮点阻塞(110010/0)：
        ——本事件记录管道因浮点冻结造成阻塞的时钟周期数。
  • 产生分支的数目(110010/1)：
        ——本事件记录发生分支的数目。
  • D1空闲且FIFO空(110011/0)，D1饿死且只有一条指令在FIFO中(110011/1)：
        ——在D1阶段，如果指令在FIFO缓冲区中有效，可以每时钟周期安排0，1或2条指令。每个事件记录了由于FIFO空造成的D1不能安排指令的次数。第二个事件记录了由于FIFO中仅有一条指令而造成的D1阶段只能安排一条指令的次数。可将执行指令(010110)和在V管道中执行的指令(010110)的两个事件合在一起。第二个事件允许用户计算由配对规则造成的两条指令不能配对的次数。
  • MMX™的数据写指令(110001/1)：
        ——类似“数据写”，只计算MMX™指令的访问。
  • 未命中的MMX™数据写指令(110100/1)：
        ——类似“未命中数据写”。只计算MMX™指令的访问。
  • 由分支预测错造成的流水线刷新，及由在WB阶段分支预测错造成的流水线刷新(110101/1)：
        ——记录任何由分支不正确造成的流水线刷新次数。包括了分支不位于BTB的情况，不论分支处于执行阶段(E阶段)还是处于回写阶段(WB阶段)。在后一种情况中，预测错误的额外开销大于一个时钟周期。上述中第一个事件记录了在E阶段或WB阶段的不正确预测的分支数目。第二个事件记录了在WB阶段非正确预测的分支数目。二者之差为E阶段的分支数目。
  • 在MMX™指令中涉及的未对齐的内存数据(110110/0)：
        ——类似“未对齐的内存数据引用”，只计算MMX™指令访问次数。
  • 由于等待读取内存数据造成的流水线阻塞(110110/1)：
        ——类似“由于等待读取内存数据造成的流水线阻塞”，只计算MMX™指令访问内存的次数。
  • 预测不正确或完全未预测到的返回数(110111/0)：
        ——该数据为预测不正确或未进行预测的实际返回指令数。它是全部执行的Return指令和正确预测的Return指令数的差值。只计算RET指令(即IRET不计算)。
  • 预测返回数(正确的和不正确的)(110111/1)：
        ——进行预测的实际返回数。只计算RET指令(即IRET指令不计算)。
  • MMX™乘法单元互锁(111000/0)：
        ——本事件记录了由于前一条MMX™乘法指令的目的数未准备好而造成的管道阻塞的时钟数。如果还有其它原因造成阻塞，计数器对此不计数。对于每个互锁乘法的发生，本事件可能记录两次(乘法后被阻塞指令持续到下一个时钟周期)也可能记录一次(乘法指令后被阻塞的指令持续两个时钟周期)。
  • 由于前一个操作造成的MOVD/MOVQ存贮阻塞(111000/1)：
        ——由于要使用前一条MMX™指令的目的数，造成了在D2阶段MOVD/MOVQ被阻塞的时钟数。
  • 返回数(111001/0)：
        ——执行Return的实际数目。只对RET指令计数(即不对IRET指令计数)。任何在RET指令执行时所发生的异常错误也将修改本计数器。
  • RSB溢出(111001/1)：
        ——本事件记录了RSB(Return Stack Buffer)没有调用地址的次数。
  • BTB入口错误(111010/0)：
        ——本事件记录了在分支目标缓存中错误入口的次数。错误入口是由未预测造成的，而不由预测错误造成的。
  • BTB未预测到的一个不发生的分支(111010/1)：
        ——本事件记录BTB将一个未发生的分支作为已发生分支预测的次数。
  • 在执行MMX™指令时，由于写缓冲区已满而造成的阻塞时钟数目(111011/0)：
        ——类似“写缓冲区满而阻塞的时钟数目”。只计算MMX™指令的访问次数。
  • MMX™指令对E或M状态行写发生的阻塞(111011/1)：
        ——类似“对E或M状态行写发生的阻塞”。只计算MMX™指令的访问次数。

6.2　动态执行(P6系列)性能监测事件

    本节描述了P6系列处理器的计数器。表4-2列出了可用来进行性能监测计数的事件，并可由RDPMC指令读出。

    在这个表中：

  • 单元栏给出了产生事件的微体系结构或总线单元。
  • 事件数栏给出了标志该事件的16进制数。
  • 事件名助记符栏给出了事件名。
  • 单元掩码栏给出了所要求的单元掩码（如果有）。
  • 描述栏描述事件。
  • 注释栏给出了有关事件的附加信息。

    这些性能监测事件用于指导性能的调节。计数器所报告的数据不保证绝对精确，但可做为性能调节的相关指导。在适当地方已标出和已知的差异。所有的性能事件都与P6系列处理器的指定模式相关，并且不保证全部存在于以后处理器的升级版本中。表中未定列出的性能事件编码全为保留，使用它们会导致未定义的计数器结果。

    进一步的细节可在本文档的升级版本中得到。

    表未端说明了表中与某些人口有关的注释。

表6-2　性能监测计数器

单元 | 事件号 | 事件名称助记符 | 单元掩码 | 描述 | 注释
数据高速缓存单元(DCU) | 43H | DATA_MEM_REFS | 00H | 全部内存引用，包括高速缓存和非高速缓存 |
 | 45H | DCU_LINES_IN | 00H | 在DCU中分配的汇总行 |
 | 46H | DCU_M_LINES_IN | 00H | DCU中已分配的M状态行数 |
 | 47H | DCU_M_LINES_OUT | 00H | 从DCU中回收的M状态行数。包括对HITM窥探，干预或替换的回收 |
 | 48H | DCU_MISS_OUTSTANDING | 00H | 当一个未命中的DCU未解决时造成的额外周期数 | 一个对L2也未命中的访问，用2个周期进行修改(即，如果计数为N个周期，则应有N+2个周期) 对同一高速缓存线的后继读取不产生额外的计数 计数值不精确，但仍然有用
指令取单元(IFU) | 80H | IFU_IFETCH | 00H | 在高速缓存和非高速缓存中取得的指令数 |
 | 81H | IFU_IFETCH_MISS | 00H | 未命中的指令提取数 |
 | 85H | ITLB_MISS | 00H | 未命中的ITLB数 |
 | 86H | IFU_MEM_STALL | 00H | 读取指令管道进程被阻塞所造成的周期数，包括高速缓存未命中、ITLB未命中、ITLB失败、回收作废的高速缓存 |
 | 87H | ILD_STALL | 00H | 指令长度译码器被阻塞所造成的周期数 |
 | 29H | L2_LD | MESI 0FH | L2数据读取的数目 |
 | 2AH | L2_ST | MESI 0FH | L2数据存贮的数目 |
 | 24H | L2_LINES_IN | 00H | 在L2中已分配的缓存线数 |
 | 26H | L2_LINES_OUT | 00H | 因任何原因，从L2中删除的缓存线数 |
 | 25H | L2_M_LINES_INM | 00H | 在L2中已分配的可修改缓存线数 |
 | 27H | L2_M_LINES_OUTM | 00H | 因任何原因，从L2中删除的可修改缓存线数 |
 | 2EH | L2_RQSTS | MESI 0FH | 申请的L2数目 |
 | 21H | L2_ADS | 00H | L2地址闸门数目 |
 | 22H | L2_DBUS_BUSY | 00H | 在数据总线忙期间的周期数 |
 | 23H | L2_DBUS_BUSY_RD | 00H | 在数据总线正在从L2传送数据到处理器期间的周期数 |
外部总线逻辑(EBL)2 | 62H | BUS_DRDY_CLOCKS | 00H 自身 20H 任意 | 全部当DRDY被保持时的时钟数目 | 单元掩码=00H 当处理器正驱动DRDY时的总线时钟计数 单元掩码=20H 当任一部件驱动DRDY时处理器内的时钟计数
 | 63H | BUS_LOCK_CLOCKS | 00H 自身 20H 全部 | 当LOCK保持时的时钟数 | 只对处理器时钟计数
 | 66H | BUS_TRAN_RFO | 00H 自身 20H 全部 | 为获得总线所有权的交换而进行的读的次数 |
 | 68H | BUS_TRAN_IFETCH | 00H 自身 20H 全部 | 因取指令而进行的数据交换的次数 |
 | 69H | BUS_TRAN_INVAL | 00H 自身 20H 全部 | 无效数据交换的次数 |
 | 6AH | BUS_TRAN_PWR | 00H 自身 20H 全部 | 不完整的写 |
 | 6BH | BUS_TRAN_P | 00H 自身 20H 全部 | 不完整的数据交换次数 |
 | 6CH | BUS_TRAN_IO | 00H 自身 20H 全部 | I/O数据交换次数 |
 | 6DH | BUS_TRAN_DEF | 00H 自身 20H 全部 | 被迟延的数据交换数 |
 | 6EH | BUS_TRAN_BURST | 00H 自身 20H 全部 | 成组数据交换数 |
 | 70H | BUS_TRAN_ANY | 00H 自身 20H 全部 | 全部数据交换 |
 | 6FH | BUS_TRAN_MEM | 00H 自身 20H 全部 | 与内存间的数据交换数 |
 | 61H | BUS_BNR_DRV | 00H 自身 | 当处理器正在驱动BNR管脚时的总线时钟周期数 |
 | 7AH | BUS_HIT_DRV | 00H 自身 | 当处理器正在驱动HIT管脚时的总线时钟周期数 | 包含由窥探阻塞造成的时钟周期
 | 7EH | BUS_SNOOP_STALL | 00H 自身 | 总线处于窥探阻塞时的时钟周期数 |
浮点单元 | C1H | FLOPS | 00H | 可计算的已退出浮点操作数目 | 仅在计数器0
 | 10H | FP_COMP_OPS_EXE | 00H | 可计算的已执行的浮点操作数目 | 仅在计数器0
 | 11H | FP_ASSIST | 00H | 由微代码处理的浮点异常情况数 | 仅在计数器1
 | 12H | MUL | 00H | 乘法数目 | 仅在计数器1
 | 13H | DIV | 00H | 除法数目 | 仅在计数器1
 | 14H | CYCLES_DIV_BUSY | 00H | 除法器忙时的周期数 | 仅在计数器0
内存序列 | 03H | LD_BLOCKS | 00H | 存贮缓冲区块数 |
 | 04H | SB_DRAINS | 00H | 存贮缓冲区所耗费的周期数 |
 | 05H | MISALIGN_MEM_REF | 00H | 对未对齐数据的引用次数 |
译码和退出 | C0H | INST_RETIRED | 00H | 退出的指令数指令 |
 | C2H | UOPS_RETIRED | 00H | 退出的微操作数 |
 | D0H | INST_DECODER | 00H | 译码的指令数 |
中断 | C8H | HW_INT_RX | 00H | 接收到的硬件中断数 |
 | C6H | CYCLES_INT_MASKED | 00H | 被禁止的中断所用的处理器时钟周期数 |
分支 | C4H | BR_INST_RETIRED | 00H | 退出的分支指令数 |
 | C5H | BR_MISS_PRED_RETIRED | 00H | 退出的未预测到的分支数目 |
 | C9H | BR_TAKEN_RETIRED | 00H | 退出的已发生分支数目 |
 | CAH | BR_MISS_PRED_TAKEN_RET | 00H | 退出的未预测到的已发生分支数目 |
 | E0H | BR_INST_DECODED | 00H | 被译码的分支指令数 |
 | E4H | BR_BOGUS | 00H | 伪分支数目 |
 | E6H | BACLEARS | 00H | 被确定的BACLEARS时间数 |
阻塞 | A2H | RESOURCE_STALLS | 00H | 与资源相关的阻塞所造成的周期数 |
 | D2H | PARTIAL_STALLS | 00H | 部分阻塞的周期或事件数目 |
读取段寄存器 | 06H | SEGMENT_REG_LOADS | 00H | 读取段寄存器的次数 |
时钟 | 79H | CPU_CLK_UNHALTED | 00H | 处理器处于非停机状态下的周期数 |

注释

    1.上述提及的几个L2高速缓存的事件，可用PerfEvtSel0和PerfEvtSel1寄存器中的单元掩码(UMSK)字段做成一步描述。单元掩码字段的低四位与L2事件一起来，指示高速缓存状态或所涉及的高速缓存状态。P6系列处理器用“MESI”协议来识别高速缓存状态，然后用单元掩码字段的每一位代表四种状态中的一种：UMSK[3]=M(8H)状态，UMSK[2]=E(4H)状态，UMSK[1]=S(2H)状态，UMSK[0]=I(1H)状态。UMSK[3:0]=MESI(FH)可用来收集全部状态数据。对于相关的事件，UMSK=0H，将不为任何事件计数。

    2.对于所有的外部总线逻辑(EBL)事件，除上述提及到的，可用PerfEvtSel0和PerfEvtSel1寄存器中的单元掩码(UMSK)字段做成一步描述。UMSK的第5位可与EBL事件一起来指明处理器是对本身产生的数据交换计数(UMAK[5]=0)，还是对在总线上其它处理器所产生的数据交换计数(UMAK[5]=1)。

6.3　RDPMC指令

    RDPMC允许用户在CPL=3，CR4的8#位被置(CR4.PCE)的情况下读取性能监测器。这方面与RDSTC(Read Time Stamp)指令相似。该指令在CPL=3时，且CR4的时间标记禁止位未被禁止时允许使用。注意，在CPL=3时不能访问性能监测控制和事件选择寄存器。

6.3.1　指令规则

    操作码：0F 33
    描述：按ECX的指定读事件监测计数器到EDX:EAX。
    操作：EDX:EAX<-事件计数器[ECX]
    在ECX中的值(0或1)指定了两个40位处理器的事件计数器之一。读取EDX的高32位和EAX的低32位。

        IF CR4.PCE=0 AND CPL<>0 THEN #GP(0)
        IF ECX=0 THEN EDX:EAX=PerfCntr0
        IF ECX=1 THEN EDX:EAX=PerfCntr1
        ELSE #GP(0)
        ENDIF

    保护模式和实地址模式异常。

    #GP(0)，如果ECX未指定一个有效计数器(0或1)。

    #GP(0)，如果在CPL<>0且CR4.PCE=0使用RDPMC指令。

    注意　16位代码：RDPMC将在16位代码和VM模式下执行，但给出一个32位结果。它使用全部ECX索引。


附录A MMX 指令集

INTEL 体系结构 MMX™ 技术开发者手册

附录A　MMX™指令集

    下表为MMX™指令集一览表。下列指令助记符是助记符基本集。大多数指令都有多种形式(如PCKED-BYTE，-WORD和-DWORD形式)。在“Intel体系结构MMX™技术程序员参考手册”(序列码为243007)中可以找到MMX™指令的全部信息。

表A-1　Intel体系结构MMX™指令集

成组算术 | 环绕方式 | 有符号饱和模式 | 无符号饱和模式
Addition | PADD | PADDS | PADDUS
Subtraction | PSUB | PSUBS | PSUBUS
Multiplication | PMULL/H |  |
Multiply&add | PMADD |  |
Shift right Arithmetic | PSRA |  |
Compare | PCMPcc |  |
转换 | 常规 | 有符号饱和模式 | 无符号饱和模式
Pack |  | PACKSS | PACKUS
Unpack | PUNPCKL/H |  |
逻辑操作 | 成组 | 全64位 |
And |  | PAND |
And not |  | PANDN |
Or |  | POR |
Exclusive or |  | PXOR |
Shift left | PSLL | PSLL |
Shift right | PSRL | PSRL |
转移及内存操作 | 32位 | 64位 |
Register-register move | MOVD | MOVQ |
Load from memory | MOVD | MOVQ |
Store to memory | MOVD | MOVQ |
其它 |  |  |
Empty multimedia state | EMMS |  |


附录B MMX 命令和操作数限制

INTEL 体系结构 MMX™ 技术开发者手册

附录B　MMX™命令和操作数限制

    在此要特别感谢震源工作室(作品：战国风云)的好朋友易春华热心补充了以下一段MMX™命令和操作数限制，使本手册变得更加完整。

MMX™命令      BYTE方式        WORD            DWORD           QWORD
PADD            *               *               *
PADDS           *               *
PADDUS          *               *
PSUB            *               *               *
PSUBS           *               *
PSUBUS          *               *
PSLL                            *               *               *
PSRA                            *               *
PSRL                            *               *               *
PAND                                                            *
PANDN                                                           *
POR                                                             *
PXOR                                                            *
PMADD                           ->
PMULH                           *
PMULL                           *
PCMPEG          *               *               *
PCMPGT          *               *               *
MOVD                                            *
MOVQ                                                            *
PACKSS          <-              <-
PACKUS          <-
PUNPCKH         ->              ->              ->
PUNPCKL         ->              ->              ->


MMX开发文档

liyuming1978

MMX开发文档

I MMX 简介

    Intel的 MMX™技术是对 Intel 体系结构(IA)指令集的扩展。该技术使用了单指令多数据技术(SIMD)技术，以并行方式处理多个数据元素，从而提高了多媒体和通讯软件的运行速度。MMX™指令集增加了 57 条新的操作码和一个新的 64 位四字数据类型。

    MMX™技术提高了很多应用程序的执行性能，例如活动图像、视频会议、二维图形和三维图形。几乎每一个具有重复性和顺序性整数计算的应用程序都可以从 MMX™技术中受益。对于 8 位、16 位和 32 位数据元素的处理，改善了程序的性能。一个 MMX™指令可一次操作 8 个字节，且在一个时钟周期内完成两条指令，也就是说，可在一个时钟周期内处理 16 个数据元素。另外，为增强性能，MMX™技术为其它功能释放了额外的处理器周期。以前需要其它硬件支持的应用程序，现在仅需软件就能运行。更小的处理器占用率给更高程度的并发技术提供了条件，在当今众多的操作系统中这些并发技术得到了利用。在基于 Intel 的分析系统中，某些功能的性能提高了 50%到 400%。这种数量级的性能扩展可以在新一代处理器中得到体现。在软件内核中，其速度得到更大的提高，其幅度为原有速度的三至五倍。

    MMX的缺点：由于 MMX 的运算指令必须在数据配对整齐的时候才能使用，所以使用 MMX 指令要比普通的汇编指令多余许多分组配对的指令，如果运算不是特别的整齐的话，就要浪费大量的时间在数据的配对上，所以说 MMX 指令也不是万能的，也有其很大的缺陷。同时 MMX 指令在处理 16 位数据的时候才能发挥最大的作用，处理 8 位数据要有一点技巧。而处理 32 位数据，MMX 指令几乎没有什么加速能力。(考虑分组耗时的话)

II MMX 基本指令集

    具体细节请参阅《INTEL 体系结构 MMX 技术程序员参考手册》第五章

2.1 拷贝指令

    movq：64 位数据拷贝，如果内存 8 位对齐的话，是一个 64 位写，否则 2 个 32 位写。

    movd：32 位数据拷贝，注意：如果从内存向 MMX 寄存器拷贝，MMX 高 32 位清零！

2.2 分组指令

    分组指令是 MMX 特有的，所以对于它我们要特别的关注。分组指令基本上可以分为 2 类，一类是不带符号紧缩的，一类是带符号紧缩的。现在我们分别予以介绍：

① punpcklbw / punpcklwd / punpckldq (l表示低位分组，bw8位，wd16位，dq32位)：它是简单的将两个MMX寄存器的低32位交错组合为一个64 位数据。所以它是不能将长数据转换为短数据的。

② packuswb 将16位数据转换为无符号的8位数据。所以可以将两个MMX寄存器不交错的合为一个64 位数据。

③ packsswb/packssdw 将 32 位－》16 位，16 位－》8 位，都是有符号的数据。

2.3 运算指令

    加法运算指令：paddb（w）（d）：没有越界保护的加法，当越界的时候仅仅丢弃超出范围的高位比特，（b）（w）（d）分别为 8，16，32 位加法；paddsb（w）：具有越界保护的有符号加法，当上溢的时候为 0x7fff，下溢的时候为 0x8000；paddusb（w）：具有越界保护的无符号加法，当上溢的时候为 0x7fff，下溢的时候为 0x0。

    减法运算指令同上；add 改为 sub。

    乘法指令：pmullw / pmulhw 是 4 个 16 位数据的乘法，pmullw 中是结果的低 16 位，pmulhw 是结果的高 16 位。pmaddwd 乘加指令。

2.4 逻辑指令，移位指令和EMMS指令

    细节参见《INTEL 体系结构 MMX 技术程序员参考手册》。

III MMX 经典处理策略

①数据输入输出：

    在输入数据的时候，经典的处理方法是将一个数组整个“Load”到 MMX 寄存器中。这样简单同时利用了 MMX64 位读写数据的能力，提高了性能。同样在输出的时候，也是将一个 64 位 MMX 寄存器中的数据内容整个“Store”到内存中。

    如果实在是不能这样处理的话，就要利用移位指令了。比如说将一个 MMX 内的 4 个 16 位数据分别拷贝到不同的内存变量（或者 16 位通用寄存器中）x1，x2，x3，x4，那么可以这样处理：

    movd eax, mm1
    psrlq  mm1, 32
    movd   ebx, mm1
    mov    x1, ax
    mov    x2, bx
    shr    eax, 16
    shr    ebx, 16
    mov    x3, ax
    mov    x4, bx

    可见如果不采用数组形式的话，输入输出将十分的麻烦。

②数据分组以及求绝对值的方法等：

    细节请参阅《INTEL 体系结构MMX™ 技术开发者手册》第五章

IV 自定义组合指令

①八位无符号数的移位：

    在 MMX 指令集中是没有 8 位数据的移位指令的，但是有的时候我们确实需要，所以可以用以下两个指令来实现：

    psrlq       mm0,1

    pand        mm0,0x7f7f7f7f7f7f7f7f

②如何防止计算过程中越界：

    比如在计算的时候，我们有（x1＋x2＋1）>>1，这个时候 x1＋x2 就会越界（8 位数据），那么我们就不得不使用替代了办法，比如（x1>>1＋x2>>1）这个处理是不精确的，在不需要很精确的场合，是可以使用的，但是如果结果差错 1 都不可容忍的话，就要进行一点处理：

    pand        mm0,0x01010101010101             // 保留数据的最后一位数

    pand        mm1,0x01010101010101             // 保留数据的最后一位数

    por         mm0,mm1

    paddusb     mmx,mm0                          // 修正数据

（x1>>2＋x2>>2）：这个处理是通用的

    pand        mm0,0x03030303030303             // 保留数据的最后两位数

    pand        mm1, 0x03030303030303            // 保留数据的最后两位数

    paddusb     mm0,mm1

    psrlq       mm0,2

    pand        mm0,0x3f3f3f3f3f3f3f3f

    paddusb     mmx,mm0

③符号扩展指令：

    mm0：*,*,A,B  =>  现在要符号扩展为 mm0：(A 符号)A, (A 符号)B

    movq      mm1, mm0

    pcgtm     mm1, 0            //比较 mm0，生成 mm1：(A 符号) (B 符号)()()

    punpcklwd mm0, mm1

④分组指令

    除了基本的分组指令以外，我们还可以利用移位指令和 pand por 指令来实现分组的功能，移位主要是要产生 0，这样 por mm0，mm1 就可以将 mm0 和 mm1 合并了。

比如：mm0(*,*,A,B) mm1(0,0,C,D) 则

    psllq mm0,32

    por mm0,mm1 => (A,B,C,D) 当然这个例子我们可以用普通的分组指令实现，但是在某些复杂的处理中，这样的处理是必须的。

总之，要灵活运用 MMX 的现有指令来实现自己需要的功能。

V MMX 编程心得

    使用 MMX 技术进行编程，目的就是要提高运算速度，所以，对于如何尽可能的提高代码的效率，我们是要特别关注的。这里，我介绍一些需要注意的事项。

① 尽可能的提高内存访问的容量，我们可以看看下面的代码：

for (j=0; j<h; j++)
{

    d[0] = s[0];

    d[1] = s[1];

    d[2] = s[2];

    d[3] = s[3];

    d[4] = s[4];

    d[5] = s[5];

    d[6] = s[6];

    d[7] = s[7];

    d[8] = s[8];

    d[9] = s[9];

    d[10] = s[10];

    d[11] = s[11];

    d[12] = s[12];

    d[13] = s[13];

    d[14] = s[14];

    d[15] = s[15];

    s+= lx2;

    d+= lx;

}

__asm{

      pushf
      mov edx,dword ptr h
      xor ecx,ecx
      mov esi,dword ptr s
      mov edi,dword ptr d
      mov eax,lx2
      mov ebx,lx

AGAIN:

      movq      mm0,byte ptr [esi]
      movq      mm1,byte ptr [esi+8]
      movq      byte ptr [edi],mm0
      movq      byte ptr [edi+8],mm1
      add       esi,eax
      add       edi,ebx
      add       ecx,1
      cmp       ecx,edx
      jl        AGAIN
      emms
      popf

}

    仅仅将几个 8 位的写，改为 64 位的写，测试得到速度提升了 25%，同样的道理，我们要尽可能的将几个 movq 写在一起，这样可以提高 5%左右的速度。原 C 代码的效率也是很高的，它不用数组的【】【】来寻址，而是将 s+= lx2; 从而将二维数组的寻址改为一维数组的寻址。尽可能的减少寻址的复杂度，这也是一种高效的办法。 还有一点就是如果将原来的简单赋值改为 memcpy（）的话，可以提高大约 10％的速度。这也是提高了数据流通容量的关系。

② 一些要注意的地方：

1. 尽可能的使用 static 变量, 访问这样的变量是很快的=访问立即数的速度

2. 由于只有一个 mmx 移位寄存器, (移位分组指令) 是不能配对的

3. 不要在 eax 使用完, 使用 ax, 不要使用完一个 mm1,就立即使用它

4. 可以这样立即使用 mm1, movq mm2,mm1 movq mm1,mm3 (Z 顺序是可以的)

5. (4 个以上)movq 尽可能的在一起, 前提是在一起的 mov 不要使用一样的 mmx 寄存器

6. mov eax, [esi] ([esi+2*eax]) 访问寻址的内存是特别的慢的

7. 同上 stow 也是很慢的 （mov cx，n; loop 是很慢的，如果可能，要展开循环）

8. 尽可能的在寄存器中完成操作,不要去访问内存

9. 用变量名访问变量,尤其是 static 的,是很快.

10. 访问寻址的内存的速度下降 》 数据不对齐 8 位的速度下降 》 指令不配对的速度下降

11. 所以在传统的代码优化的方法中，构造数组，然后将运算变为查表的方法，有的时候在 MMX 技术内反而会降低速度。（这个时候，如果真的用查表有提升速度的话，建议采用段地址＋偏移量的办法）


MMX指令集简介（一）

  GameDragon

MMX指令集简介（一）

从今天起开始介绍一下MMX指令集。

MMX技术大家肯定是如雷贯耳，也不用我多废话了。总之，它是SIMD（单指令多数据）技术的一个最简单的部分，它只能对整数（BYTE、WORD、DWORD、QWORD）进行操作，而且提供的指令也有限，不象Intel原来的指令集那么复杂。但是我们可敬的雷锋叔叔不是说过“要把有限的生命投入到无限的为人民服务之中去”么？我们这里也是一样，要把有限的指令应用到无限的计算方法当中去。简单有限的指令组合起来，一样可以完成近乎无限的功能。不信可以去Intel的网站（http://www.intel.com/software/idap/processor/ia32/mmx/）看看，那里有非常多的应用MMX技术来优化了的算法，包罗了图像、图形、音频、通讯、信号处理等几乎所有现在比较流行的技术。当然SSE和SSE2又更进了一步，不过我们现在是讨论MMX，先把这个搞定对于理解其他的SIMD技术肯定是大有帮助。

好了，废话少说，开始进入正题。
先来讲一下MMX寄存器。MMX寄存器实际上用的就是浮点寄存器（就是原来的ST0、……、ST7）的低64Bit（至少一直到PII还是这样，PIII是不是这样我就不知道了），分别被命名为MM0、MM1、……、MM7，每个寄存器都可以被看作是8个BYTE，4个WORD、2个DWORD或1个QWORD，具体以什么为单位取决于所用的指令。MMX指令可以把这几个数据在一条指令里同时操作，而且相互间各不相干，例如下图所示:
  ┌───┬───┬───┬───┐
  │　A1　│　A2　│　A3  │　A4  │
  └───┴───┴───┴───┘
  ┌───┬───┬───┬───┐
  │　B1　│　B2　│　B3  │　B4  │
 +└───┴───┴───┴───┘
 ──────────────────
  ┌───┬───┬───┬───┐
  │A1+B1 │A2+B2 │A3+B3 │A4+B4 │
  └───┴───┴───┴───┘
这对数据密集型的计算（比如图像处理）是个相当大的喜讯。但是因为它实际使用的是浮点寄存器，所以不能和浮点运算同时执行（占了人家的房子当然不好意思了），要执行浮点指令前必须执行一条EMMS指令，通知CPU“我的租期到了，现在该把房子还给你了”。这么一条指令也是要花时钟周期的（可能还绝对不止一个时钟周期），要是频繁的切换MMX状态和浮点状态，从MMX技术获得的这么点利润就又全陪进去了。所以在写代码的时候要注意，务必使MMX指令的运算和浮点运算分开来，个人管个人，免得一个房子两个人挤。

下面介绍MMX指令。
在MMX指令集里加入了几个原来Intel指令集里没有（当然没有了，我指的是没有类似的运算指令）的非常好用的指令，我以前搞过图像处理，最能理解这几个指令的好处，就先从这几个指令讲起。

1.饱和运算
所谓饱和运算，就是当运算结果大于一个上限或小于一个下限时，结果就等于上限或是下限。例如：BYTE运算，最大值是255，0xF1+0x35应该是等于0x26，但由于结果大于255，那么饱和运算的结果就是0xFF。在图像处理里经常有（比如说增加亮度）两种灰度值运算后要判断只是否大于255或小于0，根据结果再取255或0，又是if又是什么的。现在只要一条指令就OK了。
这几条指令分别是：

PADDS[B,W] | 饱和有符号数加[byte, word]
PADDUS[B,W] | 饱和无符号数加[byte, word]
PSUBS[B,W] | 饱和有符号数减[byte, word]
PSUBUS[B,W] | 饱和无符号数减[byte, word]

是不是很方便啊！（有符号数就是有正有负，一个BYTE就是-128~127；无符号数就是都是正的，一个BYTE就是0~255）
注：PADDS[B,W]的意思就是PADDSB和PADDSW的简写，以下都将这样写。

先讲到这里，下一篇再接着讲。（敲的累死了，以后肯定不写书了）


MMX指令集简介（二）

  GameDragon

MMX指令集简介（二）

（上一篇忘记说明了，这些所有的MMX指令的目的操作数——就是放在前面的——一般都是MMX寄存器，源操作数——就是放在后面的——一般是MMX寄存器或内存，以下除非有特殊说明，均有这个限制）

2.向量点乘指令
什么是向量点乘我想不用我说了吧，学过高数的都该知道。这条指令的格式如下：

PMADDWD 向量点乘运算(word)

其实际操作为：
┌───┬───┬───┬───┐
│  A1  │  A2  │  A3  │  A4  │
└───┴───┴───┴───┘
┌───┬───┬───┬───┐
│  B1  │  B2  │  B3  │  B4  │
└───┴───┴───┴───┘ PMADDWD
──────────────────────
┌───────┬───────┐
│  A1*B1+A2*B2 │  A3*B3+A4*B4 │
└───────┴───────┘

这条指令的用处不要我说了，高数里有好多这种运算。我只是感叹，这么好用的指令为什么Intel不早点想出来呢？

3.串比较指令
这类指令有两个

PCMPEQ[B,W,D] | 串等于比较(byte,word,dword)
PCMPGT[B,W,D] | 串大于比较(byte,word,dword)

举个例子说明实际操作
┌───┬───┬───┬───┐
│ 0xF0 │ 0xA2 │ 0x24 │ 0x75 │
└───┴───┴───┴───┘
┌───┬───┬───┬───┐
│ 0xF0 │ 0x2C │ 0x24 │ 0x54 │
└───┴───┴───┴───┘PCMPEQ
─────────────────────
┌───┬───┬───┬───┐
│ 0xFF │ 0x00 │ 0xFF │ 0x00 │
└───┴───┴───┴───┘

┌───┬───┬───┬───┐
│ 100  │  70  │   6  │  18  │
└───┴───┴───┴───┘
┌───┬───┬───┬───┐
│  40  │  120 │   5  │  26  │
└───┴───┴───┴───┘PCMPGT
─────────────────────
┌───┬───┬───┬───┐
│ 0xFF │ 0x00 │ 0xFF │ 0x00 │
└───┴───┴───┴───┘

看明白了？结果为全1，即说明比较结果为TRUE；结果为全0，说明比较结果为FALSE。这条指令用在什么地方？字符串快速比较（Intel网站上有源码，扬言速度提高50%）、模拟Color Key（具体怎么实现不在本文讨论范围之内，自己想一想，我以后可能会介绍一下）等等好多方面。

接下来的指令就比较普通了，但还是有非常大的便宜可以占的。

4.数据传输

MOV[D,Q] 数据传输(dword,qword)

MOVD就是传32个Bit（传到MMX寄存器的低32Bit或只传输MMX寄存器的低32Bit），MOVQ就是传64个Bit，这条指令是可以在MMX寄存器、通用寄存器（就是EAX、ESI之类，但是这些寄存器只能用在MOVD里——一直到PIII都还是32Bit的通用寄存器嘛）和内存之间来回折腾的，没什么限制。

今天就到这儿吧。


MMX指令集简介（三）

  GameDragon

MMX指令集简介（三）

5.算术指令
这里的指令和普通的算术指令没有什么运算规则上的区别，只不过是在一条指令里同时处理几个数据而已。指令如下：
PADD[B,W,D] | 无符号数加[byte,word,dword]
PADDS[B,W] | 有符号数加[byte,word]
PSUB[B,W,D] | 无符号数减[byte,word,dword]
PSUBS[B,W] | 有符号数减[byte,word]
PMULHW | 有符号数乘取高位[word]
PMULLW | 有符号数乘取低位[word]

值的提一下的是那两个乘法指令，因为两个WORD相乘，结果是个DWORD，但在MMX指令里又要保持数据的一致性，不可能是交给它4个数据，运算完了就剩下2个数据了，于是Intel就搞了这么个方法，乘法嘛，要算两次，一次只把高位给你，另一次再把低位给你——怎么，不服气么？哼，我给了你这么多方便又好用的指令，才讹了你这么点儿时钟周期就不满意了？CPU是我做还是你做？哼，没话说了吧！——没办法，人家是老大，该怎样就怎样了。不过凡事要往两方面想，假如我们要判断两数是否同号，只要乘完了取高位就行了，低位嘛，在这儿用不上，随它怎么样了，而且还省了个寄存器。
这两条指令的实际操作如下（只写一条就行了）：

┌───────┬───┬───┐
│      A1      │  …  │  …  │
└───────┴───┴───┘
┌───────┬───┬───┐
│      B1      │  …  │  …  │
└───────┴───┴───┘PMULHW
─────────────────────
┌───────┬───┬───┐
│HIWORD(A1*B1) │  …  │  …  │
└───────┴───┴───┘

但是大家发现没有，这里没有除法运算，也没有无符号数的乘法运算，不要急，PIII的SSE指令集里就有了，大家升级CPU啊！

6.逻辑指令
这些指令和原来我们学过的8086汇编里的逻辑指令完全相同（除了PANDN相当于原先的两个操作——与和非——的结合），只是把寄存器换成64位的而已。

PAND | 按位与操作
PANDN | 按位与后再取非操作
POR | 按位或操作
PXOR | 按位异或操作

7.移位指令
移位指令也不要我多说了，实际就是更方便（方便嘛当然就要有点儿限制了）的乘除法运算。但是却没有BYTE为单位的移位运算，不知道是为什么。

PSLL[W,D,Q] | 逻辑左移[word,dword,qword]
PSRL[W,D,Q] | 逻辑右移[word,dword,qword]
PSRA[W,D,Q] | 算术右移[word,dword,qword]

再提一句，这条指令的源操作数（就是放在后面的那个）可以是个立即数，就像
PSLLW MM0, 2
这样，就是每个WORD左移2位。还有，当源操作数是MMX寄存器或是内存地址时，CPU是把它当成一个数来看的，注意了，不要以为还是当分开的数据看，对应的单元移位对应的位数（我曾经以为会是这样，但事实证明我是错的），那有这种美事？

好了，今天就讲这么多吧！还有最后一类指令，留到下次吧！

补充日期: 2000-11-17 17:21:19

差点儿误人子弟，修正一下

PANDN指令的操作是：将目的数取非再和源数做与操作，用C语言表示为

dest = (~dest) & src

对不起，这条指令我原来没用过，想当然了，抱歉抱歉。


MMX指令集简介（四）

  GameDragon

MMX指令集简介（四）

8.数据转换指令
现在已经讲了绝大部分的MMX指令，大家看到了吧，每个指令前都有个“P”开头。这个“P”的意思就是——Packet，就是说每个寄存器里的数据都是个数据包，而不是一个数据。那么这些指令就是把这些数据打包或拆包（我不知该怎么下这个操作的定义）用的。这些指令的操作都有点儿怪异，倒需要详细说明一下。
PACKUSWB | 有符号数WORD带饱和压缩成无符号BYTE
PACKSS[WB,DW] | 有符号数带饱和压缩成有符号数[word->byte,dword->word]
PUNPCKH[BW,WD,DQ] | 交错放置两数的高位[byte->word,word->dword,dword->qword]
PUNPCKL[BW,WD,DQ] | 交错放置两数的低位[byte->word,word->dword,dword->qword]

先说一下PACKUSWB。举个例子，PACKUSWB MM0, MM1，其实际操作为（图示为高位在左，低位在右），

┌───┬───┬───┬───┐    ┌───┬───┬───┬───┐
MM1│  A   │  B   │  C   │  D   │    │  E   │  F   │  G   │  H   │MM0
└───┴───┴───┴───┘    └───┴───┴───┴───┘
│                                                              │
└─────────────┐      ┌─────────────┘
│      │
┌─┬─┬─┬─┬─┬─┬─┬─┐
│A │B │C │D │E │F │G │H │MM0
└─┴─┴─┴─┴─┴─┴─┴─┘
不知道大家有没有看明白。大致意思就是：目的数紧缩到低位，源操作数紧缩到高位。另外，这个指令是带饱和操作的，就是说大于255的紧缩后就变成255，小于0的紧缩后就变成0。
PACKSS[WB,DW]的操作和PACKUSWB一样，不过多了个[DW]，就是从DWORD紧缩成WORD。

再说一下拆包操作。名字虽然是UNPACK，实际好像不是PACK的逆操作，反倒有点怪，这个操作和乘法操作有一点点类似，也有高位和低位之分的。比如说：PUNPCKHBW MM0, MM1，其实际操作为：

┌─┬─┬─┬─┬─┬─┬─┬─┐    ┌─┬─┬─┬─┬─┬─┬─┬─┐
MM1│A1│B1│C1│D1│E1│F1│G1│H1│    │A2│B2│C2│D2│E2│F2│G2│H2│MM0
└─┴─┴─┴─┴─┴─┴─┴─┘    └─┴─┴─┴─┴─┴─┴─┴─┘
│                                                                  │
└──────────────┐      ┌──────────────┘
│      │
┌─┬─┬─┬─┬─┬─┬─┬─┐
│A1│A2│B1│B2│C1│C2│D1│D2│MM0
└─┴─┴─┴─┴─┴─┴─┴─┘
有了PACKUSWB作基础，大家应该看懂这个图示了吧。这明明不是什么拆包操作是吧！唉，我不过是为了讲究名称的匹配，再说它的指令助记符也写着是UNPACK嘛，不就是个名称，你又何必认真呢？PUNPCKL[BW,WD,DQ]的操作和PUNPCKH类似，就是把两个数的低位交错着放到目的数里。

OK，到此为止，MMX指令集简介正式结束，谢谢大家。


SSE 指令入门(一)



Pentium III处理器的单指令多数据流扩展指令(Streaming SIMD Extensions, SSE)入门
第一部分

概要 :
随着Intel Pentium III处理器的发布，给程序设计人员又带来了许多新的特性。利用这些新特性，程序员可以为用户创造出更好的产品. Pentium III和Pentium III Xeon(至强处理器)的许多新特性，可以使她能够比Pentium II和Pentium II Xeon处理器有更快的运行速度，这些新特性包括一个处理器序列号(unique processor ID)和新增SSE处理器指令集,这些新的指令集就像Pentium II在经典Pentium的基础上添加的MMX指令集.

在这篇文章里,我们将向你展示Pentium III处理器和她的这些新特性,并且将着重讲述Pentium III处理器的新指令集.

1.Pentium III处理器概要
1999年2月,Intel发布了她的最新款处理器Pentium III处理器,和以往的新处理器推出一样,速度的提高是最主要的性能改善.Intel在发布她的新处理器时一贯遵循着摩尔定律,即每过18个月处理器的速度将提高一倍(the processor speed doubles every 18 months),但是Pentium III处理器的并没有比Pentium II的速度提高一倍,在Pentium II和Pentium II Xeon处理器运行在333MHz~400MHz时,Pentium III也只不过运行 在450MHz~550MHz而已,处理器的速度并没有多大的提高,但是性能的提升确是很明显的.

从本质上说，Pentium III处理器只不过是一个运行在更高速度的Pentium II处理器,另外再增加了一些新的指令集:Streaming SIMD Extensions(单指令多数据流扩展指令集,或者称为SSE).这些新指令集的增加并不会影响原来的程序运行,因为Pentium III处理器采用的是完全兼容于原来Pentium II处理器的IA-32构架.

如果Pentium III处理器的速度并没有比Pentium II有倍速的提高,那我们为什么还要去选购她呢?

2.Pentium III处理器的新特性
Pentium III处理器添加了两个有趣也是很有用的新特性:处理器序列号(processor serial number)和SSE指令集.由于Pentium III处理器的序列号涉及到用户隐私的争议,为了避免这种争议,在这里我们将把注意力放在Pentium III处理器的SIMD新指令集上.

SSE包含一个SIMD的首字母，SIMD是Single Instruction Multiple Data(单指令多数据)的首字母的缩写.通常,处理器在一个指令周期只能处理一个数据,这叫做Single Instruction Single Data(单指令单数据),缩写为SIDI.和SIDI不同的是,如果处理器具有SIMD能力,那么她就可以在一个CPU指令周期同时处理多个数据.

3.MMX vs SSE
MMX和SSE都是在原来的处理器指令集的基础上添加的扩展指令集,都是SIMD(单指令多数据)指令,不同的是他们处理的数据类型不同. MMX只能在整数上支持SIMD,而SSE指令增加了单精度浮点数的SIMD支持.MMX可以进行同时对2个32位的整数操作,而SSE可以同时对4个32位的浮点数操作.

MMX和SSE的一个主要的区别是MMX并没有定义新的寄存器,而SSE定义了8个全新的128位寄存器,每个寄存器可以同时存放4个单精度浮点数(每个32位长),他们在寄存器中排列顺序见下图1.
图一:新数据类型排列

这里有一个问题,既然MMX没有定义新的寄存器,那么她又有什么寄存器可操作呢?事实上,MMX是和原来的浮点寄存器共享的.一个浮点寄存器是80位长的,她的低端64位被用做MMX的寄存器.这样,一个应用程序就不能在执行MMX指令的同时进行浮点操作了.同时,处理器还要花掉大量的时钟周期去维护寄存器状态从MMX操作和浮点操作之间的切换.SSE指令集就没有这些限制了.由于她定义了全新的寄存器,应用程序可以在进行整数SIMD操作(MMX)的同时进行浮点数的SIMD操作(SSE),同样,SSE还可以在执行浮点数的非SIMD操作的同时进行SIMD操作.

MMX和SSE的寄存器排列见下图2.图2(a)是MMX和浮点数共享一个寄存器的情况,图2(b)是SSE的独立寄存器排列.
图二:MMX和SSE的寄存器

MMX和SSE寄存器有一个共同点,那就是都有8个寄存器.MMX的寄存器被命名为mm0~mm7,SSE的寄存器名字是xmm0~xmm7.

4.程序中的应用
Pentium III的SSE指令集是为SIMD设计的,她可以同时操作4个单精度浮点值.因此,利用这些加强的浮点计算能力,对3D应用程序的细节表现是有实质性的提高的.事实上,SSE就是为3D应用创建的.游戏和其他的使用后端3D来显示2D和2.5D图象的程序,和使用矢量图形的应用程序一样都能分享到这种好处.

4.1 3D运算
 计算机表示的3D图形是用大量的表示图形顶点的浮点数组成的,通过操作这些顶点数据就可以改变3D图形的外观.通过使用SSE指令集,应用程序可以获得更多的帮助,处理器可以在一个时钟周期内处理更多的数据,大大加快了3D图形的顶点计算速度,可以给用户带来更深刻的3D体验.

同样,应用程序开发者还可以用更多的顶点数据和更复杂的算法来创造出更为生动的3D图象效果来.

使用SSE指令集可以显著的改善一些在3D操作中经常用到的计算,像矩阵乘法、矩阵变换以及矩阵之间的加、减、乘、向量矩阵相乘、矢量化、 矢量点相乘和光照计算等等.

5.单指令多数据流指令集
SSE增加了70条新的指令,同时也添加了一个状态/控制字(status/control word).SSE指令集必须要得到操作系统的支持,支持她的操作系统必须能够保存和恢复这个处理器的状态字.目前,只有Microsoft的Windows98和Windows2000支持SSE指令集.SSE定义了新的指令、新的数据类型和指令范畴.

这些新添加的指令不是全部用来浮点数SIMD操作的,在这70条新指令中,有50条是浮点数的SIMD操作的,12条是针对整数的SIMD操作的,还有8条是cache操作（cacheability ）指令.在这篇文章里,我们将着重讨论这50条浮点数的SIMD操作指令.

5.1 分类
Pentium III的SIMD新指令集可以按照不同的分类标准得到不同的分类方法.

如果我们按照指令的操作数划分,可以得到"数据包装"(Data Packing)的分类方法.
如果按照指令的动作特征划分,可以得到"指令范畴"(Instruction Categories)分类方法.
如果按照指令的计算特征划分,可以得到"指令分组"(Instruction Groups)分类方法.

5.1.1 数据包装分类
 如果我们按照指令的操作数划分浮点数的SIMD部分,我们可以得到两个明显的分类:操作包裹数据(packed data)的指令和操作标量数据(scalar data)的指令.因此,我们可以得到包裹指令(packed instructions )和标量指令(scalar instructions)这两个种类.

在Pentium III指令集中,包裹指令和标量指令可以很方便的被区分出来.包裹指令的都带有"ps"前缀,而标量指令有一个"ss"前缀. SSE还定义了一个新的数据类型(data type),可以用来储存4个单精度浮点数,这个新的数据类型可以用下面的图3来表示.图中的元素"A"包含了4 个单精度浮点数a0,a1,a2,a3.
图三:SSE的新数据类型

 上面的图三可以帮助我们理解包裹指令和标量指令之间的区别.SSE的包裹指令同时操作这个新数据类型的4个元素,而标量指令只操作这个新数据类型的最后一个元素,其他三个元素的内容将在指令执行前后保持不变.

图4说明了包裹指令的操作方法.如图所示,有两个操作数A和B,都是SSE定义的新数据类型.操作结果将被保存在数据C.C和A、B是一样的数据类型.操作数A有四个组成元素a0,a1,a2,a3,同样操作数B也有四个元素b0,b1,b2,b3.下图中的op是要进行的操作指令.

图四：包裹操作

操作指令是被分别作用于A和B的四个元素的,操作结果将被放在C的相应元素中.指令op是在同一个时间对这四个元素操作的,并在同一个计算单元中进行计算.

一个标量指令使用和上面一样的操作指令op,操作数A和B,如下图5示例说明.在这里操作指令只作用于操作数A和B的最后一个元素.同样,结果只储存在C的相应最后一个元素中.还有,在这里虽然只有一个元素被改变,操作指令op仍然同时操作所有的4个元素,并且也是在一个计算单元中进行的.

图五：标量操作

5.1.2 指令范畴分类(按照指令动作类型划分)
SSE指令集中几乎所有的包裹指令都有一个标量指令和她对应.SSE指令也可以不按操作数分类,而是把她分成下面的几个类:

· 计算指令(computation instructions)
· 分支指令(branching instructions)
· Cache指令(cacheability instructions)
· 数据移动和排序指令(data movement and ordering instructions)

5.1.3 指令组分类(按照指令计算特征分类)
SSE指令还可以被分成下面的几类:

· 算术指令(arithmetic instructions)
· 比较指令(comparison instructions)
· 逻辑指令(logical instructions)
· 清洗指令(shuffle instructions)
· 转换指令(conversion instructions)
· 状态操作指令(state management instructions)
· Cache指令(cacheability instructions)
· 数据操作指令(data management instructions)
· 附加整数指令(additional integer instructions)

5.2 优点
SSE最主要的优点是可以大大减小数据计算的指令操作数目.如果不使用SIMD和SSE,要进行一个400次的浮点数乘法计算,需要循环使用400次的乘法指令.而如果使用了SIMD和SSE,则只要进行100次的乘法指令就可以完成相同的任务了,因为这里每次的乘法操作都可以同时对4个浮点数进行计算.


SSE 指令入门(二)



Pentium III处理器的单指令多数据流扩展指令(Streaming SIMD Extensions, SSE)入门
第二部分
作者:Bipin Patwardhan 翻译:foenix

关键字 :
Pentium,处理器,单指令多数据流扩展指令,SSE,指令集

概要 :
随着Intel Pentium III处理器的发布，给程序设计人员又带来了许多新的特性。利用这些新特性，程序员可以为用户创造出更好的产品. Pentium III和Pentium III Xeon(至强处理器)的许多新特性，可以使她能够比Pentium II和Pentium II Xeon处理器有更快的运行速度，这些新特性包括一个处理器序列号(unique processor ID)和新增SSE处理器指令集,这些新的指令集就像Pentium II在经典Pentium的基础上添加的MMX指令集.

1. 使用SSE
在具体描述了SSE指令集以后,让我们看看怎样才能在应用程序中使用他们呢.

1.1 汇编语言
传统地,程序员希望能够使用汇编语言来利用新处理器的新特性.通常这是必须的,因为高级的程序开发工具只有在处理器正式推出以后的某个适当的时间才会由新版本发布支持. Pentium III的情况也是这样.现在,只有Intel的C/C++编译器和Microsoft Macro Assembler(6.11d及以上版本)才认识新的SSE指令集.

这里有一个矛盾的地方:如果用纯汇编语言来写一个大型的、复杂的应用程序是非常困难的,但是这样写出来的代码执行速度又是最快的.

我们也可以使用SSE SDK(Software Developers Kit,软件开发工具包)开发包,Intel在开发包中提供了两种编程机制去使用SSE指令集:一个intrinsics库和一个表示SSE定义的新数据类型的C++类.使用这些机制比单纯用汇编语言简单.这是很明显的,因为这样程序员再也不用由自己去管理SSE的寄存器了,可以很方便的创建出大型的应用程序.但是这种机制写出的代码又比用汇编语言写的代码执行速度慢了.图6说明了这三种开发方法在程序执行速度和开发困难程度之间的矛盾.

图六: 在不同开发环境下的程序执行速度和开发困难程度之间的矛盾

1.1.1 示例:乘法
假设有两个128位的数a和b分别存储在寄存器xmm1和xmm2中,他们的计算结果保存在寄存器xmm0中.用C语言嵌入汇编的代码如下:

#include
...
_asm {
push esi;
push edi;
; a is loaded into xmm1
; b is loaded into xmm2
mov xmm0, xmm1;
mulps xmm0, xmm2;
; store result into c
pop edi;
pop esi;
}
...

图7用图表来表示了这种包裹乘法(packed multiplication)的计算

图七:包裹乘法计算

1.2 Intrinsics库
 使用Intrinsics库是第一种附加的编程机制.Intrinsics库为C语言提供了一个使用SSE指令集的接口.所有的SSE指令在这个库中都被包装成了C函数.两个包裹数据(packed data)相加的汇编指令是addps.相应的,在intrinsics库中将两个包裹数据相加的对应函数是_mm_add_ps.为了配合这些新增的函数,intrinsics库还定义了一个新的数据类型(__m128)来表示128位长的数据,可以用来保存4个单精度浮点数.

要使用intrinsics库,还需要在程序中包含(include)xmmintrin.h头文件.

1.2.1 示例:乘法
假设有两个128位数a和b,他们的计算结果将被保存在另一个128位数c中.a,b和c都是__m128数据.__m128是128位数据类型,已经在头文件xmmintrin.h中定义了.函数_mm_set_ps功能是把他的4个参数按照第一个参数为最高位、最后一个参数为最低位的顺序排列组合成一个128数.

#include
...
__m128 a, b, c;
a = _mm_set_ps(4, 3, 2, 1)
b = _mm_set_ps(4, 3, 2, 1)
c = _mm_set_ps(0, 0, 0, 0)
c = _mm_mul_ps(a, b);
...

1.3 C++
第二种附加的编程机制是使用C++语言.SSE SDK开发包提供了一个C++类:F32vec4,用来处理和表示一个128位的新数据类型.所有的新数据类型的操作都被封装在这个类中了.在类的内部,他也是使用intrinsics库的.

要使用这个C++类,,还必须在程序中包含(include)fvec.h头文件.

1.3.1 示例:乘法
 我们再次假设有两个128位的数a和b,他们的计算结果放在另一个128位数c中.所有的数据都定义成F32vec4类.类的构造函数功能就相当于_mm_set_ps函数.

#include ...
F32vec4 a(4, 3, 2, 1), b(4, 3, 2, 1), c(0, 0, 0, 0);
...
c =a * b;
...

1.4 编译器支持
前面已经说过,只有Intel的C\C++编译器和Microsoft的Macro Assembler支持新的SSE指令集.Intel编译器已经整合到Microsoft的Visual Studio集成开发环境中了.Visual Studio集成开发环境可以被配置成使用Intel的编译器来编译整个工程或者工程中的某个文件.

2. SSE指令详细资料
在我们介绍SSE指令用法的例子以前,让我们先来看看SSE指令集的所有指令列表.

Arithmetic Instructions(算术指令)
addps, addss
subps, subss
mulps, mulss
divps, divss
sqrtps, sqrtss
maxps, maxss
minps, minss

Logical Instructions(逻辑指令)
andps
andnps
orps
xorps

Compare Instructions(比较指令)
cmpps, cmpss
comiss
ucomiss

Shuffle Instructions(清洗指令)
shufps
unpchkps
unpcklps

Conversion Instructions(转换指令)
cvtpi2ps, cvtpi2ss
cvtps2pi, cvtss2si

Data Movement Instructions(数据移动指令)
movaps
movups
movhps
movlps
movmskps
movss

State Management Instructions(状态管理指令)
ldmxcsr
fxsave
stmxscr
fxstor

Cacheability Control Instructions(cache控制指令)
maskmovq
movntq
movntps
prefetch
sfence

Additional SIMD Integer Instructions(附加的SIMD整数指令)
pextrw
pinsrw
pmaxub, pmaxsw
pminub, pminsw
pmovmskb
pmulhuw
pshufw

3. 例子
在这一节,我们将介绍几个例子来帮你理解Pentium III的SSE指令集应用.在每个例子中,我们都将介绍三种解决方案,分别使用汇编语言、intrinsics库和C++类.附加例子将在下一部分的附加示例节中介绍.

3.1 包裹乘法
在前面我们已经用三种不同的开发机制介绍了两个包裹数据的乘法计算,这三种机制分别是使用汇编语言、intrinsics库和C++类来编写.

3.2 比较操作
 让我们来考虑一下比较的事件.如果不使用SSE,我们每次只能对一对浮点数进行比较.使用了SSE以后,可以同时对4对浮点数进行比较.

在C和C++中比较4对浮点数,我们可以像下面一样写一个循环,把每次的比较放在循环里面.另外我们还需要定义保存比较结果的变量.代码可以类似于下面的例子.

float a[4], b[4]
int i, c[4];
// assume that a contains 4.5 6.7 2.3 and 1.2
// assume that b contains 4.3 6.9 2.0 and 1.5
for (i = 0;i < 4; i++ )
c[i] = a[i] < b[i];
// take action on comparison result

在SSE指令集中,比较指令是cmpps,她有两个128位的操作数和一个选项参数.选项参数是用来指明指令比较类型的:是大于、小于、大于等于还是小于等于的比较.cmpps指令用来比较4对浮点数的大小,并将比较结果放在第一个操作数中.如果比较的结果是真,相对应的元素将被置为FFFFFFFF,如果为非,则被置为00000000.这个结果还可以被映射到一个普通的(8位或者16位)寄存器中,这个普通的寄存器用一个位来映射SSE寄存器的一个元素.我们可以从这个映射的寄存器来得到比较的结果.

下面举例来说明cmpps指令的用法.

; assume that xmm0 contains 4.5 6.7 2.3 and 1.2
; assume that xmm1 contains 4.3 6.9 2.0 and 1.5

; compare for less than condition
cmpps xmm0, xmm1, 1;

; move result of comparison as a mask into eax
movmskps eax, xmm0;

; test eax against some value
test eax, 5;

; jump if true, to the given label
je match13

这个操作也可以用图8的图示来表示.

图八 :比较运算
3.3 分支移除
通常,在程序中我们喜欢用下面的条件语句.

a =(a < b) ? c :d;

在上面的代码中,比较操作影响着后面代码的执行路径.如果我们能够移除条件判断,程序将能执行的更快.下面的这段汇编代码将比上面的代码执行得更快,这不仅仅是因为下面的代码是用汇编语言写的,更重要的是这里的分支判断已经被移除了.

; assume that xmm0 stores a, xmm1 stores b
; assume that xmm3 stores c and xmm4 stores d

cmpps xmm0, xmm1, 1;
movaps xmm2, xmm0;
andps xmm0, xmm3;
andnps xmm2, xmm4;
orps xmm0, xmm2;

; xmm0 contains the result, which is either c or d


SSE 指令入门(三)



Pentium III处理器的单指令多数据流扩展指令(Streaming SIMD Extensions, SSE)入门
第三部分

概要 :
随着Intel Pentium III处理器的发布，给程序设计人员又带来了许多新的特性。利用这些新特性，程序员可以为用户创造出更好的产品. Pentium III和Pentium III Xeon(至强处理器)的许多新特性，可以使她能够比Pentium II和Pentium II Xeon处理器有更快的运行速度，这些新特性包括一个处理器序列号(unique processor ID)和新增SSE处理器指令集,这些新的指令集就像Pentium II在经典Pentium的基础上添加的MMX指令集.

1. Data Swizzling
要利用Pentium III处理器SSE指令的加速功能也是要有一定的代价的.因为SSE指令只能操作她定义的新数据类型(128位).如果应用程序使用自己的数据类型格式,那么在进行SSE指令操作之前要将他转换为这种新数据类型,操作完成之后又要把他转换回来.

这种把一种数据格式转换到另一种数据格式的操作就称为"data swizzling".

这种转换是需要时间和耗费处理器核心周期的.如果一个应用程序频繁的进行数据格式转换,处理器核心周期的浪费是很严重的.因此,这种数据格式的转换是必须得到关注的.

1.1 数据组织
通常,3D应用程序将一个顶点保存在一个和他相配的数据结构中.当表达多个顶点时,应用程序使用这个结构的数组,也叫做AoS来表示.一个典型的操作是对表示顶点的x、y和z坐标进行的.下面的代码就给出了这样一个表示3D顶点的数据结构.如果要表示大量的这样的顶点,就要用到这个结构的一个数组了,如图9所示.

struct point {
float x, y, z;
};
...
point dataset[...];

图九 :结构数组

SSE的优点,就是可以同时对多个顶点的进行处理.这样我们就必须要能够方便的处理到表示多个顶点的数据(比如表示4个顶点x坐标的4个浮点数).这是可以实现的,我们可以把表示一个顶点的x、y和z三个坐标值分别集合在一起,然后应用程序对他们进行处理.要实现这些,应用程序必须重新组织数据到三个单独的数组中,或者创建一个数组结构,结构中每个数组对应一个这样的坐标值数组.这个数据结构也称为SoA结构.(我是这样理解的: 在不用SSE时是把表示一个顶点的三个坐标值组合到一个数据结构中,处理时一个值一个值的进行.而使用SSE以后,可以把所有点的各坐标值分别组合到3个数组中,处理时取出这样一个数组的4个值同时执行).

下面的代码就定义了这样的一个数组结构,如图10用图表来表示的数组结构.

struct point {
float *x, *y, *z;
};

图十: 数组结构

2. 内存问题
2.1 对齐
简单的处理和使用新数据类型变量是没有问题的.然而,一个推荐的做法是使用16字节边界对齐来定义新数据类型变量.可以通过设置一个特定的编译器参数或者在变量声名时显式的使用一个对齐标记来实现.

声名变量时可以指定__declspec编译器标记来强制变量使用16字节边界对齐,就像下面的例子代码所示.下面的变量myVar在使用这个对齐标记以后将被强制到16字节边界对齐.但当在直接声明这个新数据类型变量(也就是SSE定义的128位新数据类型)时就不需要指定这个对齐标记了,编译器会在遇到这个新数据类型时自动按16字节对齐.下面是对齐标记的用法 :

__declspec(align(16)) float[4] myVar;

2.2 动态分配
 由于新数据类型要求的16字节边界对齐,使得在动态分配内存或者用指针来分配内存时会产生问题.

当使用指针访问数组时,我们必须保证这个指针是16字节边界对齐的.

在运行时分配内存可以使用malloc或者new命令.默认情况下这两种方法分配的指针都不是16字节边界对齐的.因此我们必须把这个分配得到的指针转换到16字节对齐,或者使用_mm_malloc函数来分配内存.使用_mm_malloc函数分配的内存块是按16字节边界对齐的. 和malloc函数有一个free函数一样,_mm_malloc函数也有一个对应的函数_mm_free.分配内存块使用_mm_malloc函数而清除(free)内存块用_mm_free函数.

2.3 自定义数据类型
指针必须是16字节边界对齐的限制很麻烦,如果能够不理睬这种限制就好了.

在使用128位新数据类型时,我们经常需要去访问储存在里面的(4个)浮点数.在使用汇编语言时我们没有许多的选择.但在使用C或者C++和intrinsics库时,我们总是用__mm128数据类型来表示这个128位的数据.使用这种数据类型(__mm128)时,一旦这个数值被设置以后,我们就不大可能直接去访问存储在里面的单独的浮点数了.有一个途径是把存储在里面的浮点数转移到一个浮点数数组里面,修改他们的数值以后再把他们转换回去.另一种途径是把这个新的数据类型映射到一个浮点数数组的方法来访问这些必要的元素.上面的第一种方法比较浪费时间而第二种方法如果使用不当则会发生问题.

定义一个自定义的数据类型能够避免这个问题.这个自定义的数据类型用联合(union)的方法把新数据类型(__mm128)和一个4个元素的浮点数数组关联起来.下面给出代码,现在可以用sse4来声名新数据类型了.

union sse4 {
__m128 m;
float f[4];
};

使用上面定义的数据类型,声明变量时不必使用对齐标记来强制16字节对齐,因为编译器在遇到这个数据联合里面的__m128数据类型时会自动按16字节对齐来分配内存的.这个数据类型的一个好处就是可以直接访问到存储在里面的单独的浮点数.

2.4 侦测CPU
使用SSE指令集需要有Pentium III处理器,应用程序一个首要的任务就是要去检查有没有Pentium III芯片.这可以用cpuid的指令完成.

想要cpuid工作,需要把eax寄存器设置为特定的值.这里我们只想得到CPU的ID,需要把eax寄存器置为1然后调用cpuid指令. 下面给出检查PentiumIII处理器是否存在的代码.要想使下面的代码工作,还必须包含fvec.h头文件.

BOOL CheckP3HW()
{
BOOL SSEHW = FALSE;
_asm {
// Move the number 1 into eax - this will move the
// feature bits into EDX when a CPUID is issued, that
// is, EDX will then hold the key to the cpuid
mov eax, 1

// Does this processor have SSE support?
cpuid

// Perform CPUID (puts processor feature info in EDX)
// Shift the bits in edx to the right by 26, thus bit 25
// (SSE bit) is now in CF bit in EFLAGS register.
shr edx,0x1A

// If CF is not set, jump over next instruction
jnc nocarryflag

// set the return value to 1 if the CF flag is set
mov [SSEHW], 1

nocarryflag:
}
return SSEHW;
}

SSE SDK还有一个Pentium III和SSE寄存器的异常模式.用下面的代码也可以用来检查是否有Pentium III处理器.要编译下面的代码,需要包含fvec.h头文件.

// Checking for SSE emulation support
BOOL CheckP3Emu()
{
BOOL SSEEmu = TRUE;
Fvec32 pNormal = (1.0, 2.0, 3.0, 4.0);
Fvec32 pZero = 0.0;
// Checking for SSE HW emulation
__try {
_asm {
// Issue a move instruction that
      will cause exception
// w/out HW support emulation
movups xmm1, [pNormal]

// Issue a computational
      instruction that will cause
// exception w/out HW support
      emulation
divps xmm1, [pZero]
}
}
// If there''s an exception, set emulation variable to false
__except(EXCEPTION_EXECUTE_HANDLER) {
SSEEmu = FALSE;
}
return SSEEmu;
}

3. 参考
For more details about the architecture of the Pentium III, refer [11], [12], [13] and [10].
For more information about Processor identification and CPUID, refer [15] and [7].
For more information about the Streaming SIMD Extensions, refer [19].
For more information about the programming issue, the software conventions and the software development strategies, refer [9], [16] and [17] respectively.
For more about application tuning for SSE and the VTune performance enhancement application, refer [1] and [8] respectively.
For more details about VTune and the Intel C/C++ Compiler, refer [3] and [2] respectively.
For additional information about the Pentium III processor and its capabilities, refer [14], [18], [20], [6], [5] and [4].

4. 附加示例
在这一节,我们将向你介绍几个SSE指令集(Streaming SIMD Extensions)使用的例子.

4.1 数组操作
下面的例子中,我们创建了两个数组,每个包含400个浮点数.将两个数组的每个元素相乘,结果保存在第三个数组中.前面两个数组作为操作数A和B,保存结果的数组为C.下面给出所有的代码.

#include < FVEC.H >
#define ARRSIZE 400
__declspec(align(16)) float a[ARRSIZE], b[ARRSIZE], c[ARRSIZE];

4.1.1 Assembly Language

_asm {
push esi;
push edi;
mov edi, a;
mov esi, b;
mov edx, c;
mov ecx, 100;
loop:
movaps xmm0, [edi];
movups xmm1, [esi];
mulps xmm0, xmm1;
movups [edx], xmm0;
add edi, 16;
add esi, 16;
add edx, 16;
dec ecx;
jnz loop;
pop edi;
pop esi;
}

4.1.2 Intrinsics

__m128 m1, m2, m3;
for ( int i = 0; i < ARRSIZE; i += 4 )
{
m1= _mm_loadu_ps(a+i);
m2= _mm_loadu_ps(b+i);
m3= _mm_mul_ps(m1,m2);
_mm_storeu_ps(c+i, m3);
}

4.1.3 C++

F32vec4 f1, f2, f3;

for ( int i = 0; i < ARRSIZE; i +=4 )
{
loadu(f1, a+i);
loadu(f2, b+i);
f3 = f1 * f2;
storeu(c+i, f3);
}

4.2 矢量3D
下面的例子介绍3D矢量.矢量被封装在一个类中.在下面类的函数里面调用了intrinsics库.

类声明.

union sse4 {
    __m128 m;
    float f[4];
};

class sVector3 {
protected:
    sse4 val;
public:
    sVector3(float, float, float);
    float& operator [](int);
    sVector3& operator +=(const sVector3&);
    float length() const;
friend float dot(const sVector3&, const sVector3&);
};

类实现.

sVector3::sVector3(float x, float y, float z) {
    val.m = _mm_set_ps(0, z, y, x);
}
float& sgmVector3::operator [](int i) {
    return val.f[i];
}
sVector3& sVector3::operator +=(const sVector3& v) {
    val.m = _mm_add_ps(val.m, v.val.m);
    return *this;
}
float sVector3::length() const {
    sse4 m1;
    m1.m = _mm_sqrt_ps(_mm_mul_ps(val.m, val.m));
    return m1.f[0] + m1.f[1] + m1.f[2];
}
float dot(const sVector3& v1, const sVector3& v2) {
    sVector3 v(v1);
    v.val.m = _mm_mul_ps(v.val.m, v2.val.m);
    return v.val.f[0] + v.val.f[1] + v.val.f[2];
}

4.3 4x4矩阵
下面的例子介绍一个4x4的矩阵.矩阵被封装在一个类中.在下面类的函数里面调用了intrinsics库.

类声明.

float const sEPSILON = 1.0e-10f;

union sse16 {
    __m128 m[4];
    float f[4][4];
};

class sMatrix4 {
protected:
    sse16 val;
    sse4 sFuzzy;
public:
    sMatrix4(float*);
    float& operator()(int, int);
    sMatrix4& operator +=(const sMatrix4&);
    bool operator ==(const sMatrix4&) const;
    sVector4 operator *(const sVector4&) const;
private:
    float RCD(const sMatrix4& B, int i, int j) const;
};

类实现.

sMatrix4::sMatrix4(float* fv) {
    val.m[0] = _mm_set_ps(fv[3], fv[2], fv[1], fv[0]);
    val.m[1] = _mm_set_ps(fv[7], fv[6], fv[5], fv[4]);
    val.m[2] = _mm_set_ps(fv[11], fv[10], fv[9], fv[8]);
    val.m[3] = _mm_set_ps(fv[15], fv[14], fv[13], fv[12]);
    float f = sEPSILON;
    sFuzzy.m = _mm_set_ps(f, f, f, f);
}
float& sMatrix4::operator()(int i, int j) {
    return val.f[i][j];
}
sMatrix4& sMatrix4::operator +=(const sMatrix4& M) {
    val.m[0] = _mm_add_ps(val.m[0], M.val.m[0]);
    val.m[1] = _mm_add_ps(val.m[1], M.val.m[1]);
    val.m[2] = _mm_add_ps(val.m[2], M.val.m[2]);
    val.m[3] = _mm_add_ps(val.m[3], M.val.m[3]);
    return *this;
}
bool sMatrix4::operator ==(const sMatrix4& M) const {
    int res[4];
    res[0] = res[1] = res[2] = res[3] = 0;
    res[0] = _mm_movemask_ps(_mm_cmplt_ps(_mm_sub_ps(
        _mm_max_ps(val.m[0], M.val.m[0]),
        _mm_min_ps(val.m[0], M.val.m[0])), sFuzzy.m));
    res[1] = _mm_movemask_ps(_mm_cmplt_ps(_mm_sub_ps(
        _mm_max_ps(val.m[1], M.val.m[1]),
        _mm_min_ps(val.m[1], M.val.m[1])), sFuzzy.m));
    res[2] = _mm_movemask_ps(_mm_cmplt_ps(_mm_sub_ps(
        _mm_max_ps(val.m[2], M.val.m[2]),
        _mm_min_ps(val.m[2], M.val.m[2])), sFuzzy.m));
    res[3] = _mm_movemask_ps(_mm_cmplt_ps(_mm_sub_ps(
        _mm_max_ps(val.m[3], M.val.m[3]),
        _mm_min_ps(val.m[3], M.val.m[3])), sFuzzy.m));
    if ( (15 == res[0]) && (15 == res[1])
            && (15 == res[2]) && (15 == res[3]) )
        return 1;
    return 0;
}
sVector4 sMatrix4::operator *(const sVector4& v) const {
    return sVector4(
        val.f[0][0] * v[0] + val.f[0][1] * v[1]
            + val.f[0][2] * v[2] + val.f[0][3] * v[3],
        val.f[1][0] * v[0] + val.f[1][1] * v[1]
            + val.f[1][2] * v[2] + val.f[1][3] * v[3],
        val.f[2][0] * v[0] + val.f[2][1] * v[1]
            + val.f[2][2] * v[2] + val.f[2][3] * v[3],
        val.f[3][0] * v[0] + val.f[3][1] * v[1]
            + val.f[3][2] * v[2] + val.f[3][3] * v[3]);
}
float sMatrix4::RCD(const sMatrix4& B, int i, int j) const {
    return val.f[i][0] * B.val.f[0][j] + val.f[i][1] * B.val.f[1][j]
        + val.f[i][2] * B.val.f[2][j] + val.f[i][3] * B.val.f[3][j];
}


SSE 介紹

Ping-Che Chen

SSE 介紹

文档内容简介：
    SSE（為 Streaming SIMD Extensions 的縮寫）是由 Intel 公司，在 1999 年推出 Pentium III 處理器時，同時推出的新指令集。如同其名稱所表示的，SSE 是一種 SIMD 指令集。所謂的 SIMD 是指 single instruction, multiple data，也就是一個指令同時對多個資料進行相同的動作。較早的 MMX 和 AMD 的 3DNow! 也都是 SIMD 指令集。因此，SSE 本質上是非常類似一個向量處理器的。SSE 指令包括了四個主要的部份：單精確度浮點數運算指令、整數運算指令（此為 MMX 之延伸，並和 MMX 使用同樣的暫存器）、Cache 控制指令、和狀態控制指令。這裡主要是介紹浮點數運算指令和 Cache 控制指令。

目录：
     [Part 1]
     [Part 2]
     [Part 3]
     [Part 4]
     [Part 5]
     [Part 6]
     [Part 7]
     [Part 8]

文档内容：

[Part 1]

    SSE 新增了八個新的 128 位元暫存器，xmm0 ~ xmm7。這些 128 位元的暫存器，可以用來存放四個 32 位元的單精確度浮點數。SSE 的浮點數運算指令就是使用這些暫存器。和之前的 MMX 或 3DNow! 不同，這些暫存器並不是原來己有的暫存器（MMX 和 3DNow! 均是使用 x87 浮點數暫存器），所以不需要像 MMX 或 3DNow! 一樣，要使用 x87 指令之前，需要利用一個 EMMS 指令來清除暫存器的狀態。因此，不像 MMX 或 3DNow! 指令，SSE 的浮點數運算指令，可以很自由地和 x87 指令，或是 MMX 指令共用。但是，這樣做的主要缺點是，因為多工作業系統在進行 context switch 時，需要儲存所有暫存器的內容。而這些多出來的新暫存器，也是需要儲存的。因此，既存的作業系統需要修改，在 context switch 時，儲存這八個新暫存器的內容，才能正確支援 SSE 浮點運算指令。下圖是 SSE 新增的暫存器的示意圖：

    目前 Intel 新推出的 x86 CPU 均已支援 SSE 指令集，包括 Pentium III、Pentium 4、Celeron 533A 及之後的處理器。AMD 方面，在其 Athlon 處理器上，新增的 Enhanced 3DNow! 指令集中，即包含了 SSE 中的整數運算指令和 Cache 控制指令，但仍不包含浮點數指令和狀態控制指令。但在最新的 Athlon XP 處理器上，已將 SSE 中的浮點運算指令和狀態控制指令加入。因此 Athlon XP 已經是完全支援 SSE 指令集了。

[Part 2]
    SSE 的浮點數運算指令，大致上可以分成兩種：packed 和 scalar。Packed 指令是一次對 XMM 暫存器中的四個浮點數（即 DATA0 ~ DATA3）均進行計算，而 scalar 則只對 XMM 暫存器中的 DATA0 進行計算。如下圖所示：

SSE packed and scalar operations

SSE 指令和一般的 x86 指令很類似，基本上包括兩種定址方式：reg-reg 和 reg-mem。下面是兩個例子：

  1. addps xmm0, xmm1 ; reg-reg
  2. addps xmm0, [ebx] ; reg-mem

指令的運算結果會覆蓋到第一個參數中。例如，以上面的第一個例子來說，xmm0 暫存器會存放最後計算的結果。

    另外，絕大部份需要存取記憶體的 SSE 指令，都要求位址是 16 的倍數（也就是對齊在 16 bytes 的邊上）。如果不是的話，就會導致 exception。這是非常重要的。因為，一般的 32 位元浮點數只會對齊在 4 bytes 或 8 bytes 的邊上（根據 compiler 的設定而不同）。另外，若是處理陣列中的數字，也需要特別注意這個問題。

[Part 3]
    目前要在 C 或 C++ 程式中，利用 SSE 指令，有很多方法。最一般的方式，是利用內嵌式組合語言。現在包括 Intel C++ Compiler 5.0 和 Microsoft Visual C++ 6.0 Processor Pack 的內嵌式組合語言都支援 SSE 指令集。另外，它們也都支援 SSE 指令集的 intrinsics。Intrinsics 是一種外表類似一般的函數，但是實際上會被 compiler 直接編譯成組合語言的東西。以下是一些例子：

內嵌式組合語言：

__asm addps xmm0, xmm1
__asm movaps [ebx], xmm0
...

__m128 data;
...
__asm
{
lea ebx, data
addps xmm0, xmm1
movaps [ebx], xmm0
}
使用 intrinsics：

__m128 data1, data2;
...

__m128 out = _mm_add_ps(data1, data2);
...

使用 intrinsics 可以增加程式的可讀性，也比較容易使用。不過，在某些情形下，compiler 可能沒辦法產生最好的程式碼，而且，其產生的程式碼的效率，也會隨著 compiler 的不同而有改變。但是，對於大部份的應用來說，使用 intrinsics 的好處通常是很明顯的。因此，在這裡我們也以 intrinsics 為主，而不討論內嵌式組合語言。

SSE 的 intrinsics 的名稱看起來雖然怪異，但是它是有一定的規則的。基本上一個 SSE 的 intrinsics 的型式為：

_mm_<opcode>_<suffix>
其中，<opcode> 是指令的類別，像是 add、sub 等等。而 <suffix> 則是資料的種類。在 SSE 浮點運算指令中，只有兩個種類：ps 和 ss。ps 是指 Packed Single-precision，也就是這個指令對暫存器中的四個單精度浮點數進行運算。而 ss 則是指 Scalar Single-precision，也就是這個指令只對暫存器中的 DATA0 進行運算。所以，像上面的 _mm_add_ps 指令，就是把兩個四維向量相加的指令。

SSE 也需要一個新的資料型態，也就是上面所看到的 __m128。__m128 是一個 16 bytes（128 bits）的資料型態，對應 SSE 的 128 位元暫存器。幾乎所有的 SSE 浮點運算的 intrinsics 都是使用這個資料型態。比如說，_mm_add_ps 這個 intrinsic 的函數宣告為：

__m128 _mm_add_ps(__m128 a, __m128 b);
可以看到，它的參數和傳回值的型態都是 __m128。基本上，這個 intrinsic 的動作就是把兩個參數相加，並把結果以傳回值的方式傳回。

另外，要使用 SSE 的 intrinsics 之前，要記得先包含 xmmintrin.h 這個 header 檔。如下：

#include <xmmintrin.h>
這樣才能使用這些 instrinsics。另外，這個檔案也包含了一些方便的巨集定義，用來配合一些 SSE 的 intrinsics 使用。

[Part 4]
現在我們就先以一個很簡單的例子，來說明 SSE 浮點運算指令的使用。下面是一個簡單的程式片斷：

  • float input1[4] = { 1.2f, 3.5f, 1.7f, 2.8f };
  • float input2[4] = { -0.7f, 2.6f, 3.3f, -0.8f };
  • float output[4];
  •
  • for(int i = 0; i < 4; i++) {
    • output[i] = input1[i] + input2[i];
  • }
  •

這個程式片斷的動作非常簡單，只是把 input1 和 input2 中的四組數字兩兩相加，並把結果寫到 output 中。在執行完後，output 的內容應該是 { 0.5f, 6.1f, 5.0f, 2.0f }。

上面的程式很適合用 SSE 浮點運算指令來做。它所進行的四個加法運算，剛好可以用一個 SSE 浮點運算指令，也就是 addps 來完成。以下是修改後的程式：
  • float input1[4] = { 1.2f, 3.5f, 1.7f, 2.8f };
  • float input2[4] = { -0.7f, 2.6f, 3.3f, -0.8f };
  • float output[4];
  •
  • __m128 a = _mm_load_ps(input1);
  • __m128 b = _mm_load_ps(input2);
  • __m128 t = _mm_add_ps(a, b);
  • _mm_store_ps(output, t);
  •

上面的程式中，先用兩個 _mm_load_ps 把浮點數資料載入 __m128 的變數中。要注意的是，這裡是假設這兩個浮點數陣列都是對齊在 16 bytes 的邊上。如果不是的話，就不能用 _mm_load_ps 這個 intrinsic 來載入，而要改用 _mm_loadu_ps 這個 intrinsic。它是專門用來處理沒有對齊在 16 bytes 邊上的資料的。但是，它的速度會比較慢。

另外，因為 x86 的 little endian 特性，位址較低的 byte 會放在暫存器的右邊。也就是說，若以上面的 input1 為例，在載入到 XMM 暫存器後，暫存器中的 DATA0 會是 1.2，而 DATA1 是 3.5，DATA2 是 1.7，DATA3 是 2.8。如果需要以相反的順序載入的話，可以用 _mm_loadr_ps 這個 intrinsic。當然，在這個例子中，順序並不影響結果，所以不需要利用這個 intrinsic。

一般來說，宣告一個 float 的陣列，並不會對齊在 16 bytes 的邊上。如果希望它會對齊在 16 bytes 的邊上，以便利用 SSE 指令的話，Visual C++ 6.0 Processor Pack 和 Intel C++ compiler 都可以指定對齊方式。指定的方式如下：

  • __declspec(align(16)) float input1[4];

在上例中，input1 會被對齊在 16 bytes 的邊上。這樣就可以直接用較快的 _mm_load_ps 來載入資料了。因為 SSE 浮點數指令常常需要資料對齊在 16 bytes 的邊上，所以在 xmmintrin.h 也定義了一個巨集 _MM_ALIGN16，是同樣的意義。因此，上面的程式也可以寫成：

  • _MM_ALIGN16 float input1[4];

利用 SSE 浮點數指令會帶來多大的差別呢？對 1,024 個浮點數進行測試的結果，結果如下（使用 Intel C++ Compiler 5.0 for Windows 編譯，在 Intel Pentium III 1.0B Ghz 上執行）：

SSE Performance Test #1

可以看到利用 SSE 浮點運算可以得到大約兩倍於 x87 浮點運算的效率。事實上，因為 Pentium III 架構上的因素，所以，如果同時使用加法和乘法運算，最多可以得到四倍的效率。在後面會有一個例子。

[Part 5]
看了簡單的例子之後，我們現在列出一些 SSE 浮點運算指令所支援的一些基本的運算：

指令 | Intrinsic | 功能
addps/addss | _mm_add_ps/_mm_add_ss | 加法
subps/subss | _mm_sub_ps/_mm_sub_ss | 減法
mulps/mulss | _mm_mul_ps/_mm_mul_ss | 乘法
divps/divss | _mm_div_ps/_mm_div_ss | 除法
sqrtps/sqrtss | _mm_sqrt_ps/_mm_sqrt_ss | 平方根
maxps/maxss | _mm_max_ps/_mm_max_ss | 逐項取最大值
minps/minss | _mm_min_ps/_mm_min_ss | 逐項取最小值

這些運算基本上都符合 IEEE 754 中的規範，根據其計算結果，設定適當的旗標（divide-by-zero、invalid 等等），或是產生 exception。這可以由一個 MXCSR 暫存器來設定。MXCSR 暫存器是一個 32 位元的旗標暫存器，可以設定是否要產生各種 exception，並會記錄上次的計算中，發生了哪些情況。下面是 MXCSR 暫存器的說明圖：

以下是 MXCSR 各欄位的說明：

欄位 | 名稱 | Intrinsic 巨集 | 說明
IE | Invalid Operation Flag | _MM_EXCEPT_INVALID | 運算中發生 invalid operation
DE | Denormal Flag | _MM_EXCEPT_DENORMA | 試圖載入 denormal value 或對 denormal value 進行運算
ZE | Divide-by-Zero Flag | _MM_EXCEPT_DIV_ZERO | 運算中發生 divide by zero
OE | Overflow Flag | _MM_EXCEPT_OVERFLOW | 運算中發生 overflow
UE | Underflow Flag | _MM_EXCEPT_UNDERFLOW | 運算中發生 underflow
PE | Precision Flag | _MM_EXCEPT_INEXACT | 運算結果不精確（inexact）
DAZ | Denormals Are Zeros | N/A | 強制將 denormals 視為 0
IM | Invalid Operation Mask | _MM_MASK_INVALID | 關閉 invalid operation exception
DM | Denormal Operation Mask | _MM_MASK_DENORM | 關閉 denormal operation exception
ZM | Divide-by-Zero Mask | _MM_MASK_DIV_ZERO | 關閉 divide by zero exception
OM | Overflow Mask | _MM_MASK_OVERFLOW | 關閉 overflow exception
UM | Underflow Mask | _MM_MASK_UNDERFLOW | 關閉 underflow exception
PM | Precision Mask | _MM_MASK_INEXACT | 關閉 inexact exception
RC | Rounding Control | N/A | 設定捨去方式
FZ | Flush to Zero | N/A | 打開 Flush-to-zero 模式

上表中有幾個地方是需要特別注意的：

  1. DAZ 若設為 1，則會開啟 Denormals Are Zeros 模式。當此模式開啟時，若來源參數中有任何 denormals，都會被視為 0，而且不會設定 DE 或產生 denormal operation exception。這個模式和 IEEE 754 規範不相容，但是通常會得到更好的效率。另外 DAZ 目前只在 Intel Pentium 4 和 Intel Xeon 處理器上有支援。有關判斷 DAZ 是否支援的方式，請參考 IA-32 Intel Architecture Software Developer's Manual, Volume 1: Basic Architecture, Section 11.6.3。
  2. RC 可以設定成四種方式：00 為 Round to nearest (even)，即將運算結果捨去至最接近數值。如果兩個數值同樣接近，則捨去至偶數。01 為 Round down，即往負無限大方向捨去。10 為 Round up，即往正無限大方向捨去。11 則是 Round toward zero (truncate)，即往零的方向捨去。
  3. FZ 若設為 1，則會開啟 Flush-to-zero 模式。當此模式開啟，且 underflow exception 被關閉時，若在運算時發生 underflow，則結果為 0（符號同真正結果），且會設定 PE 和 UE。若 underflow exception 未關閉，則此模式無任何影響。注意此模式和 IEEE 754 規範不相容（規範要求 underflow 需產生 denormal 結果），但通常會得到更好的效率。

設定和讀取 MXCSR 同樣有 intrinsic 可以用，即 _mm_getcsr 和 _mm_setcsr。不過，因為 MXCSR 基本上是由 bit field 組成，因此在 xmmintrin.h 裡面也定義了一些方便的巨集。這些巨集包括：

巨集 | 功用
_MM_SET_EXCEPTION_STATE | 設定 exception 狀態（IE ~ PE）
_MM_GET_EXCEPTION_STATE | 取得 exception 狀態（IE ~ PE）
_MM_SET_EXCEPTION_MASK | 設定 exception mask（IM ~ PM）
_MM_GET_EXCEPTION_MASK | 取得 exception mask（IM ~ PM）
_MM_SET_ROUNDING_MODE | 設定捨去方式
_MM_GET_ROUNDING_MODE | 取得目前的捨去方式
_MM_SET_FLUSH_ZERO_MODE | 設定 Flush-to-zero 模式（_MM_FLUSH_ZERO_ON 和 _MM_FLUSH_ZERO_OFF）
_MM_GET_FLUSH_ZERO_MODE | 取得 Flush-to-zero 模式設定

捨去方式的設定包括：

_MM_ROUND_NEAREST | Round to nearest (even)
_MM_ROUND_DOWN | Round down
_MM_ROUND_UP | Round up
_MM_ROUND_TOWARD_ZERO | Round toward zero (truncate)

在開機時，所有的 exception mask 都是設定為 1，也就是所有的 exception 都是關閉的。如果有需要的話，可以將其開啟。下面的程式片斷是一個例子：
  • _MM_ALIGN16 float test1[4] = { 0, 0, 0, 1 };
  • _MM_ALIGN16 float test2[4] = { 1, 2, 3, 0 };
  • _MM_ALIGN16 float out[4];
  •
  • _MM_SET_EXCEPTION_MASK(0); // 打開所有的 exception
  •
  • __try {
    • __m128 a = _mm_load_ps(test1);
    • __m128 b = _mm_load_ps(test2);
    • a = _mm_div_ps(a, b);
    • _mm_store_ps(out, a);
  • }
  • __except(EXCEPTION_EXECUTE_HANDLER) { // exception handler
    • if(_mm_getcsr() & _MM_EXCEPT_DIV_ZERO) {
      • cout << "Divide by zero" << endl;
    • }
    •
    • return;
  • }
  •
  • cout << out[3] << endl;

上面的程式在執行時，會產生 divide by zero 的 exception，並進入 exception handler。在 exception handler 中，因為 ZE 會被設定，因此會顯示出 "Divide by zero" 的錯誤訊息。如果把 _MM_SET_EXCEPTION_MASK(0); 這一行去掉，則不會產生 exception，而會直接輸出正無限大（1.#INF）的結果。

[Page 6]
現在我們來看看一個實際的例子，是一個 3D 繪圖運算中很常需要進行的動作，也就是三維向量的內積。

兩個三維向量 <x1, y1, z1> 和 <x2, y2, z2> 的定義為：

x1x2 + y1y2 + z1z2

但是 SSE 浮點運算指令是以四維向量為單位的，而且也沒有計算內積的指令。要直接對一個三維向量計算出內積，會需要相當多的資料重排動作，反而會浪費很多時間。

而且，如果三維向量在記憶體中是以 x1, y1, z1, x2, y2, z2, ... 的方式排列的話，還會有對齊的問題。因為一個三維向量只需要 12 bytes，所以無法讓每個向量都對齊在 16 bytes 的邊上。這也會降低效率。

一個簡單的方法是改變資料的排列方式。如果我們把資料改成以下面的方式排列：

x1, x2, x3, x4, ...
y1, y2, y3, y4, ...
z1, z2, z3, z4, ...

那我們就同時解決了兩個問題。首先，因為一次讀入四個 x 值（及 y 值和 z 值），所以不會有對齊的問題。另外，運算也變得容易。下面的程式片斷一次可以算出四個向量內積：

  • __m128 x1 = _mm_load_ps(vec1_x);
  • __m128 y1 = _mm_load_ps(vec1_y);
  • __m128 z1 = _mm_load_ps(vec1_z);
  •
  • __m128 x2 = _mm_load_ps(vec2_x);
  • __m128 y2 = _mm_load_ps(vec2_y);
  • __m128 z2 = _mm_load_ps(vec2_z);
  •
  • __m128 t1 = _mm_mul_ps(x1, x2);
  • __m128 t2 = _mm_mul_ps(y1, y2);
  • t1 = _mm_add_ps(t1, t2);
  • t2 = _mm_mul_ps(z1, z2);
  • t1 = _mm_add_ps(t1, t2);
  •
  • _mm_store_ps(output, t1);

由於 SIMD 指令的特性，很多時候，不要把工作當成向量來考慮。而應該是把多個工作，結合到一個向量中來進行。以上面的例子來說，一個三維向量的內積，是很難有效率地用 SSE 指令來完成的。事實上，配合 SSE 的資料重排指令（後面會介紹），一個四維向量的內積，需要八個 SSE 浮點運算指令才能完成。這並不是很有效率。但是，如果我們不要以向量為單位，而改成考慮一次對四個向量進行運算的話（如同上面的例子），四個三維向量的內積，由五個指令就可以完成了。四個四維向量的內積也只需要七個指令。

以下是以 1,024 個三維向量，以上述的資料排列方式，在 Intel Pentium III 1.0B Ghz 上測試的結果（使用 Intel C++ Compiler 5.0 for Windows 編譯）：

SSE DOT3 test result

可以看到使用 SSE 浮點運算指令，得到的效率比使用 x87 浮點數要高出甚多，幾乎達到三倍。

[Page 7]
    前面的向量內積的測試，是以 1,024 個三維向量，對一個固定的三維向量進行內積得到的結果。它的資料量算是相當小的，甚至可以整個放到 L1 cache 裡面。但是，實際情形常常不是這樣。我們常常需要對一大堆資料進行運算，因此資料是無法放在 cache 裡面的，而需要從主記憶體中取得。這樣一來，速度會變得相當的慢。下面是對 102,400 個三維向量進行同樣動作的結果：

SSE test with large data set

可以看到效率差了很多，這是因為資料都需要從主記憶體中讀取，因而讀取的時間成為瓶頸，而不是計算的時間。在這種情形下，SSE 浮點運算指令速度雖快，也沒有什麼幫助。而且，因為內積的計算是相當簡單的，所以這也讓 SSE 和 x87 的結果差距變得很小。

不過，現在我們會討論到 SSE 的 "streaming" 部份。SSE 除了運算的指令之外，還支援了一些 cache 控制指令。我們在這裡先介紹兩個較簡單的指令：prefetch 和 movntps。prefetch 指令實際上有四個不同的指令，包括 prefetch0、prefetch1、prefetch2、和 prefetchnta。不過，它們都是用同一個 intrinsic 表示的，也就是 _mm_prefetch。

prefetch 指令的主要目的，是提前讓 CPU 載入稍後運算所需要的資料。通常是在對目前的資料進行運算之前，告訴 CPU 載入下一筆資料。這樣就可以讓目前的運算，和載入下一筆資料的動作，可以同時進行。如果運算的複雜度夠高的話，這樣可以完全消除讀取主記憶體的 latency。不同的 prefetch 指令則是告訴 CPU 將資料載入不同層次的 cache。不過，最常用的還是 prefetchnta，這個指令會把資料載入到離 CPU 最近的 cache 中（通常是 L1 cache 或 L2 cache），適用於資料在短時間內就會用到的情形。

另外 prefetch 指令不會產生任何 exception。它本質上只是一個 hint，CPU 並不一定會真的進行載入的動作。所以，即使 prefetch 一個不合法的記憶體位址，也不會產生錯誤。這讓程式可以不用處理討厭的邊界問題。

除了 prefetch 之外，另一個指令是 movntps，它的 intrinsics 是 _mm_stream_ps。這個指令的用途，是要求 CPU 在寫入資料的時候，不要把資料寫到 cache 中，而是直接將資料寫到主記憶體中。實際上它以 write combining 的方式寫入的。為什麼要這樣做呢？這是因為，很多時候計算的結果並不是立刻需要用到的，通常是很久以後才會用到。所以，這些資料如果被放在 cache 中，完全是浪費空間。而且，更糟的是，它們可能會把 cache 中有用的資料擠掉，而使得這些資料常常需要重新從主記憶體中載入。因此，如果讓這些資料不要被放在 cache 中，就可以避免這種問題。

對上面的程式，加上適當的 prefetch，並利用 movntps 指令，可以修改成類似下面的程式片斷：

  • __m128 x1 = _mm_load_ps(vec1_x);
  • __m128 y1 = _mm_load_ps(vec1_y);
  • __m128 z1 = _mm_load_ps(vec1_z);
  •
  • __m128 x2 = _mm_load_ps(vec2_x);
  • __m128 y2 = _mm_load_ps(vec2_y);
  • __m128 z2 = _mm_load_ps(vec2_z);
  •
  • _mm_prefetch((const char*)(vec1_x + next), _MM_HINT_NTA);
  • _mm_prefetch((const char*)(vec1_y + next), _MM_HINT_NTA);
  • _mm_prefetch((const char*)(vec1_z + next), _MM_HINT_NTA);
  •
  • _mm_prefetch((const char*)(vec2_x + next), _MM_HINT_NTA);
  • _mm_prefetch((const char*)(vec2_y + next), _MM_HINT_NTA);
  • _mm_prefetch((const char*)(vec2_z + next), _MM_HINT_NTA);
  •
  • __m128 t1 = _mm_mul_ps(x1, x2);
  • __m128 t2 = _mm_mul_ps(y1, y2);
  • t1 = _mm_add_ps(t1, t2);
  • t2 = _mm_mul_ps(z1, z2);
  • t1 = _mm_add_ps(t1, t2);
  •
  • _mm_stream_ps(output, t1);

同樣對 102,400 個三維向量進行測試，結果為：

SSE streaming test result with large data set

由於 x87 無法利用 movntps 指令，所以效率較差。可以看到，加上 prefetch 和 movntps 指令後，SSE 浮點運算的效率提高了約 50%。事實上，它所使用的記憶體頻寬達到約 500MB/s，已經是理論頻寬（1,066MB/s）的一半左右了。

[Page 8]
    前面介紹的計算都是蠻簡單的計算。現在我們來看一個比較複雜的例子。假設現在有一堆數字，都是單精確度的浮點數（float），而且範圍都在正負 0.5 之間。然後現在要計算它們的自然指數（exp）。當然我們知道 SSE 並沒有這個指令。事實上除了簡單的四則運算（加、減、乘、除）之外，SSE 和計算有關的指令，只有平方根而已。其它複雜的東西像是各種超越函數都是沒有的。

    不過，這並不表示要算超越函數就不能用 SSE 了。這只是表示我們得自己算而已。以自然指數來說，最簡單的算法當然是用 Taylor series。事實上有更快的算法，不過我們為了舉例方便，所以就直接用 Taylor series 了。因為我們的數字範圍是在正負 0.5 之間，所以只需要取到第九項，也就是 x 的八次方項，就已經很接近單精確度浮點數所能表示的精確度了。下面是利用 Taylor series 算 exp(x) 的程式片斷：

  • int i;
  • float result = coeff[8] * x;
  •
  • for(i = 7; i >= 2; i--) {
    • result += coeff[i];
    • result *= x;
  • }
  •
  • return (result + 1) * x + 1;

上面的程式片斷中，coeff 陣列需要事先設定成 1/n! 的值。這可以事先算好，直接放在程式裡面，所以不需要花時間。這個程式片斷是利用 Horner 法來計算 Taylor series，以減少計算量。在上例中，每個 X 需要 8 個乘法和 8 個加法，共 16 個計算。

這個程式要改成用 SSE 來寫，並不會很困難。不過，coeff 陣列需要修改成 __m128 的陣列，而且每項存放四個相同的數字（也就是 1/n!）。這樣可以改成下面的程式：

  • int i;
  • __m128 X = _mm_load_ps(data);
  • __m128 result = _mm_mul_ps(coeff_sse[8], X);
  •
  • for(i = 7; i >=2; i--) {
    • result = _mm_add_ps(result, coeff_sse[i]);
    • result = _mm_mul_ps(result, X);
  • }
  •
  • result = _mm_add_ps(result, sse_one);
  • result = _mm_mul_ps(result, X);
  • result = _mm_add_ps(result, sse_one);
  •
  • _mm_store_ps(out, result);

上面的程式片斷和 x87 的版本長得幾乎一樣，當然它是一次對四個浮點數做運算，所以應該會快四倍。下圖是測試結果：

SSE Taylor series test

這些測試結果是對 1,024 個隨機產生的數字進行的。第一個結果是直接用 exp 函式，它裡面是用 x87 的指令像 F2XM1 和 FYL2X 等指令來完成計算的。當然這個函式並沒有範圍的限制，而且它得到的結果也是最精確的。不過，它的速度非常的慢，平均每個數字需要 128 個 cycles 才能算完。第二個結果是用 x87 版的 Taylor series 來計算，也就是第一個程式片斷。它很明顯要快得多，平均每個數字只需要 66 個 cycles，幾乎是快了一倍。不過，其實這裡就產生一個問題：為什麼 16 個計算會需要 66 個 cycles 才能完成？

第三個結果是用 SSE 計算 Taylor series，也就是第二個程式片斷。它比 x87 版本快了近四倍，平均每個數字只需要 17 cycles。可是，因為 SSE 指令一次對四個數字進行運算，所以其實每四個數字共花了 68 cycles。這裡也是同樣的問題：為什麼 16 個計算會需要 68 cycles？這表示平均一個 SSE 乘法指令或加法指令，花了四個 cycles 執行。但是，理論上 Pentium III 不是可以每 cycle 執行一個 SSE 指令嗎？

也許問題是出在上面的程式片斷裡面的迴圈（for(i = 7; i >=2; i--)）。所以，也許可以把它展開試試。它會得到上面的第四個結果，實際上也是快了不少，平均每個四個數字花了約 43 cycles。也就是說，平均每個運算花了 2.7 cycles。這樣已經算是相當不錯了。可是，它和「每 cycle 執行一個 SSE 指令」還有一段距離。

事實上，這是因為這些指令都有相依性。在上面的程式片斷中，result 這個變數不但從頭用到尾，而且每個指令都用到這個變數，也會修改這個變數。因此，第二個指令要等到第一個指令執行完後，才能執行。在實際執行時，因為 Pentium III 的 instruction reorder buffer 夠大，所以可以達到一部份程度的平行執行，但是效率還不是非常理想。

要儘可能達到平行執行的效率，就要用另一個方法來展開迴圈。也就是說，不展開內部的迴圈，而是一次對多組資料進行計算。因為多組資料的計算之間並沒有相依性，要平行執行就容易多了。上圖中的第五個結果，就是一次對四組數字進行計算（也就是共 16 個數字），而內部的迴圈並沒有展開。這樣平均每四個數字只需要 34 cycles，也就是平均每個運算花了約 2.1 cycles。比起第四個結果，又要再快了一些。

那如果一次對八組數字進行計算，會再變快嗎？答案是不會，它反而會變慢。為什麼呢？因為 SSE 只有八個浮點數暫存器，所以如果一次對八組數字進行運算，那一定會有些東西無法放到暫存器中，而需要放到記憶體裡面。雖然這些資料幾乎是一定會放在快速的 L1 cache 中，但是它還是會需要一些額外的 load/store 指令。所以反而不會增加效率。不過，我們還是可以把內部的迴圈也展開，因為這個迴圈並不大，所以展開並不會太誇張。把內部的迴圈也展開後，得到的就是第六個結果，也就是平均每四個數字只需要 22.6 cycles，平均每個運算只需要 1.4 cycles！這已經離「每 cycle 執行一個 SSE 指令」不遠了！

還有辦法再加快嗎？恐怕不容易了。因為它已經達到約 2,837MFLOPS（在 Pentium III 1.0B Ghz 上執行）。事實上，這個結果比前面計算向量內積的速度還要快。所以，要再加快應該是很難了。

所以，這裡要討論的重點就是，當計算相當冗長，而且相依性很高的時候，光是展開內部的迴圈是不夠的，應該要考慮也展開外部的迴圈（也就是一次對多組資料進行同樣的計算）。這樣通常比展開內部的迴圈要更有效。當然，如果情況許可的話，也可以連內部的迴圈也展開，通常可以達到更高的效率。

另外，可能會有人問：這樣計算自然指數，精確度有多高呢？和用 exp 函數比較又是如何？在這個例子中，1,024 個隨機數字中，誤差都小於 1.2E-7，也就是單精確度浮點數的 epsilon。這表示有誤差的情形，都只有最後一個位元不同。因此，這應該已經是非常高的精確度了。


MMX 指令简明表

EMMS
 MMX状态置空:
将FP特征字置空(全1),使后续浮点指令可以使用浮点寄存器,其他MMX指令自动置FP为全0.本指令应在所有MMX例程结束和调用可含有FP指令的例程时使用,以清除MMX状态.
MOVD mm,r/m32
MOVD r/m32,mm 		转移32位数据:
将32位数据从整型寄存器/内存移到MMX寄存器,和反向移动.MOVD不能在MMX寄存器之间,内存之间及整型寄存器之间移动数据.目标操作数为MMX寄存器时,32位源操作数写入目标寄存器的低32位.目标寄存器"0扩展"为64位.源操作数为MMX寄存器时,该寄存器的低32位被写入目标操作数.
MOVQ mm,r/m64
MOVQ r.m64,mm 		转移64位数据:
将64位数据从整型寄存器/内存移到MMX寄存器,和反向移动.目标操作数和源操作数可为MMX寄存器,64位内存操作数.但MOVQ不能在内存和内存之间进行数据转移.

PACKSSWB mm,mm/m64
PACKSSDW mm,mm/m64

有符号饱和方式数据成组:
将MMX寄存器和MMX寄存器/内存单元中的有符号字组变成MMX寄存器的有符号字节组.和将MMX寄存器和MMX寄存器/内存单元中的有符号双字组变成MMX寄存器的有符号字组.(注1)
PACKUSWB mm,mm/m64 无符号饱和方式数据成组
将MMX寄存器和MMX寄存器/内存单元中的有符号字组变成MMX寄存器的无符号字节组.(注1)

PADDB mm,mm/m64
PADDW mm,mm/m64
PADDD mm,mm/m64

环绕方式数据组相加:
按环绕方式将MMX寄存器/内存单元中的字节组(字组,双字组)相加到MMX寄存器中(注1)

PADDSB mm,mm/m64
PADDSW mm,mm/m64

饱和方式有符号数据组相加:
按饱和方式将MMX寄存器/内存单元中的有符号字节组(字组)相加到MMX寄存器中的有符号字节组(字组)数据.(注1)

PADDUSB mm,mm/m64
PADDUSW mm,mm/m64

饱和方式无符号数据组相加:
按饱和方式将MMX寄存器/内存单元中的无符号字节组(字组)相加到MMX寄存器中的无符号字节组(字组)数据.(注1)

PAND mm,mm/m64

逐位逻辑与:
将MMX寄存器/内存单元中的64位数据进行与操作,结果存于MMX寄存器中.

PANDN mm,mm/m64

逐位逻辑与非:
将MMX寄存器中的64位值取反,再将取反后的MMX寄存器与MMX寄存器/内存单元中的64位数据进行与操作,结果存于MMX寄存器中.

PCMPEQB mm,mm/m64
PCMPEQW mm,mm/m64
PCMPEQD mm,mm/m64

成组数据的相等比较:
将MMX寄存器与MMX寄存器/内存单元中的字节组(字组,双字组)数据进行相等比较.
该指令将目标操作数和源操作数的相应数据元素进行比较,相等则目标寄存器的对应数据元素被置为全1,否则置为全0.
eg:PCMPEQE mm,mm/m64

mm | ? | ? | 00000000000000111 | 0111000111000111
mm/m64 | ? | ? | 11111110000001100 | 0111000111000111
结果mm | ? | ? | 00000000000000000 | 1111111111111111

PCMPGTB mm,mm/m64
PCMPGTW mm,mm/m64
PCMPGTD mm,mm/m64

成组数据的相等比较:
将MMX寄存器与MMX寄存器/内存单元中的字节组(字组,双字组)数据进行大于比较.
该指令将目标操作数和源操作数的相应数据元素进行比较,大于则目标寄存器的对应数据元素被置为全1,否则置为全0.(参考上一条)
PMADDWD mm,mm/m64 数据组(字组)的乘加:
将MMX寄存器与MMX寄存器/内存单元中的字组数据相乘,然后将32位结果逐对相加并作为双字存于MMX寄存器中.
eg:PMADDWD mm,mm/m64
mm | ? | ? | 0111000111000111 | 0111000111000111
操作 | * | * | * | *
mm,mm/m64 | ? | ? | 1000000000000000 | 0000010000000000
操作 | \_____+____/ |  | \______+_____/
mm | ? | ? | 1100100011100011 | 1001110000000000

PMULHW mm,mm/m64

成组数据(字组)的乘后取高位:
将MMX寄存器与MMX寄存器/内存单元中的有符号字组数据相乘,然后将结果的高16位存入MMX寄存器.
eg:PMULHW mm,mm/m64
mm | ? | ? | 0111000111000111 | 0111000111000111
操作 | * | * | * | *
mm/m64 | ? | ? | 1000000000000000 | 0000010000000000
操作 | High Order | High Order | High Order | High Order
mm | ? | ? | 1100011100011100 | 0000000111000111

PMULLW mm,mm/m64

成组数据(字组)的乘后取低位:
将MMX寄存器与MMX寄存器/内存单元中的有符号字组数据相乘,然后将结果的低16位存入MMX寄存器.(参考上一条)
POR mm,mm/m64 逐位逻辑或:
将MMX寄存器/内存单元中的64位数据进行或操作,结果存于MMX寄存器中.

PSLLW mm,mm/m64
PSLLD mm,mm/m64
PSLLQ mm,mm/m64
PSLLW mm,imm8
PSLLD mm,imm8
PSLLQ mm,imm8

成组数据的逻辑左移:
将MMX寄存器中的字(双字,四字)数据按MMX寄存器/内存单元指定的个数左移,低位移入0.
将MMX寄存器中的字(双字,四字)数据按8位立即数指定的个数左移,低位移入0.
PSRAW mm,mm/m64
PSRAD mm,mm/m64
PSRAW mm,imm8
PSRAD mm,imm8 		成组数据的算术右移:
将MMX寄存器中的字(双字)数据按MMX寄存器/内存单元指定的个数右移,移动中保持符号位.
将MMX寄存器中的字(双字)数据按8位立即数指定的个数右移,移动中保持符号位.
PSRLW mm,mm/m64
PSRLD mm,mm/m64
PSRLQ mm,mm/m64
PSRLW mm,imm8
PSRLD mm,imm8
PSRLQ mm,imm8 		成组数据的逻辑右移:
将MMX寄存器中的字(双字)数据按MMX寄存器/内存单元指定的个数右移,移出位用0填充.
将MMX寄存器中的字(双字)数据按8位立即数指定的个数右移,移出位用0填充.
PSUBB mm,mm/m64
PSUBW mm,mm/m64
PSUBD mm,mm/m64 		环绕方式成组数据相减:
从MMX寄存器中按字节(字,双字)减去MMX寄存器/内存单元中的字节(字,双字)组.(注1)

PSUBSB mm,mm/m64
PSUBSW mm,mm/m64

饱和方式有符号成组数据相减:
从MMX寄存器中的有符号成组字节(字)组数据减去MMX寄存器/内存单元中的有符号字节(字)组数据.(注1)
PSUBUSB mm,mm/m64
PSUBUSW mm,mm/m64 		饱和方式有符号成组数据相减:
从MMX寄存器中的无符号成组字节(字)组数据减去MMX寄存器/内存单元中的无符号字节(字)组数据.(注1)
PUNPCKHBW mm,mm/m64
PUNPCKHWD mm,mm/m64
PUNPCKHDQ mm,mm/m64 		高位成组数据分解:
此指令交替取出源操作数和目标操作数的数据元素的高半部分,写入目标操作数中,数据元素的低半部分被忽略.
eg:PUNPCKHBW mm,mm/m64
PUNPCKLBW mm,mm/m64
PUNPCKLWD mm,mm/m64
PUNPCKLDQ mm,mm/m64 		低位成组数据分解:
此指令交替取出源操作数和目标操作数的数据元素的低半部分,写入目标操作数中,数据元素的高半部分被忽略.(参考上一条)
PXOR mm,mm/m64 逐位逻辑异或:
将MMX寄存器/内存单元中的64位数据进行异或操作,结果存于MMX寄存器中.

< 注1: >
环绕模式vs饱和模式
在环绕式处理中,运算结果上溢或下溢后,溢出的高位被切断,只保留低位(最低有效位),并作为运算结果返回.即:进位位被丢掉.在饱和式处理模式中,上溢或下溢的运算结果被按照该数据类型的边界进行裁剪,超过上限被置为最大值,低于下限被置为最小值.这种模式可以使颜色处理中颜色值保持纯白色或纯黑色.

饱和方式的数据取值范围
:-) | 下限 |  | 上限
有符号 | 十六进制 | 十进制 | 十六进制 | 十进制
字节(Byte) | 80H | -128 | 7FH | 127
字(Word) | 8000H | -32,768 | 7FFFH | 32,767
无符号 | 十六进制 | 十进制 | 十六进制 | 十进制
字节(Byte) | 00H | 0 | FFH | 255
字(Word) | 0000H | 0 | FFFFH | 65,535


SSE指令简明参考
作 者： 蒋黎

[ A_[4] / C_[9] / D_[2] / E_[1] / F_[2] / L_[1] / M_[15] / O_[1] / R_[4] / S_[5] / U_[3] / X_[1] ]

    这一部分将简单介绍SSE指令的功能，如果你想详细了解每条指令的具体数据，可以去查阅Intel的参考手册。
  ADDPS
说明 | ADDPS xmm1, xmm2/m128
功能 | 两组单精度数相加
算法 　DEST[31-0]　 = DEST[31-0]　 + SRC/m128[31-0]
  DEST[63-32]　= DEST[63-32]　+ SRC/m128[63-32];
  DEST[95-64]　= DEST[95-64]　+ SRC/m128[95-64];
  DEST[127-96] = DEST[127-96] + SRC/m128[127-96];

  ADDSS
说明 | ADDSS xmm1, xmm2/m32
功能 | 低位单精度数相加
算法 　DEST[31-0]　 = DEST[31-0] + SRC/m32[31-0];
  DEST[63-32]　= DEST[63-32];
  DEST[95-64]　= DEST[95-64];
  DEST[127-96] = DEST[127-96];

ANDNPS
说明 | ANDNPS xmm1, xmm2/m128
功能 | xmm1“取反”再和 xmm2/m128 求“与”运算
算法 | DEST[127-0] = NOT (DEST[127-0]) AND SRC/m128[127-0];

ANDPS
说明 | ANDPS xmm1, xmm2/m128
功能 | 进行两个寄存器的逻辑“与”操作
算法 | DEST[127-0] AND = SRC/m128[127-0];
  CMPPS
说明 CMPPS xmm1, xmm2/m128, imm8
功能 比较两个寄存器的数值，根据imm8的不同数值采用不同的比较方法
    imm8 == 0, ==; imm8 == 1, <; imm8 == 2, <=; imm8 == 3, ?;
    imm8 == 4, !=; imm8 == 5, !<; imm8 == 6, !<=; imm8 == 7, !?;
算法 　IF (imm8 = 0)
      THEN OP = "EQ";
  ELSEIF (imm8 = 1)
      THEN OP = "LT";
  ELSEIF (imm8 = 2)
      THEN OP = "LE";
  ELSEIF (imm8 = 3)
      THEN OP = "UNORD";
  ELSEIF (imm8 = 4)
      THEN OP = "NE";
  ELSEIF (imm8 = 5)
      THEN OP = "NLT";
  ELSEIF (imm8 = 6)
      THEN OP = "NLE";
  ELSEIF (imm8 = 7)
      THEN OP = "ORD";
  FI

  CMP0 = DEST[31-0]　 OP SRC/m128[31-0];
  CMP1 = DEST[63-32]　OP SRC/m128[63-32];
  CMP2 = DEST [95-64] OP SRC/m128[95-64];
  CMP3 = DEST[127-96] OP SRC/m128[127-96];

  IF (CMP0 = TRUE) THEN
      DEST[31-0] = 0XFFFFFFFF;
  ELSE
      DEST[31-0] = 0X00000000;
  FI
  IF (CMP1 = TRUE) THEN
      DEST[63-32] = 0XFFFFFFFF;
  ELSE
      DEST[63-32] = 0X00000000;
  FI
  IF (CMP2 = TRUE) THEN
      DEST[95-64] = 0XFFFFFFFF;
  ELSE
      DEST[95-64] = 0X00000000;
  FI
  IF (CMP3 = TRUE)
      THEN DEST[127-96] = 0XFFFFFFFF;
  ELSE
      DEST[127-96] = 0X00000000;
  FI
其它 你可以使用下面的可读性良好的指令。

  指令　　　　　　　　　　　实现
  CMPEQPS　　xmm1, xmm2;　　　CMPPS xmm1,xmm2, 0
  CMPLTPS　　xmm1, xmm2;　　　CMPPS xmm1,xmm2, 1
  CMPLEPS　　xmm1, xmm2;　　　CMPPS xmm1,xmm2, 2
  CMPUNORDPS xmm1, xmm2;　　　CMPPS xmm1,xmm2, 3
  CMPNEQPS　 xmm1, xmm2;　　　CMPPS xmm1,xmm2, 4
  CMPNLTPS　 xmm1, xmm2;　　　CMPPS xmm1,xmm2, 5
  CMPNLEPS　 xmm1, xmm2;　　　CMPPS xmm1,xmm2, 6
  CMPORDPS　 xmm1, xmm2;　　　CMPPS xmm1,xmm2, 7

CMPSS
说明 | CMPSS xmm1, xmm2/m32, imm8
功能 | 低位单精度数做比较
算法 | 算法同CMPPS相似，只不过只是针对DEST[31-0]进行操作。
其它 | 同样也可以利用可读性更好的指令 指令 实现 CMPEQSS xmm1, xmm2 CMPSS xmm1,xmm2, 0 CMPLTSS xmm1, xmm2 CMPSS xmm1,xmm2, 1 CMPLESS xmm1, xmm2 CMPSS xmm1,xmm2, 2 CMPUNORDSS xmm1, xmm2 CMPSS xmm1,xmm2, 3 CMPNEQSS xmm1, xmm2 CMPSS xmm1,xmm2, 4 CMPNLTSS xmm1, xmm2 CMPSS xmm1,xmm2, 5 CMPNLESS xmm1, xmm2 CMPSS xmm1,xmm2, 6 CMPORDSS xmm1, xmm2 CMPSS xmm1,xmm2, 7
  COMISS
说明 | COMISS xmm1, xmm2/m32
功能 | 比较低位数并且设置标识位
算法 　OF = 0;
  SF = 0;
  AF = 0;
  IF ((DEST[31-0] UNORD SRC/m32[31-0]) = TRUE) THEN
      ZF = 1;
      PF = 1;
      CF = 1;
  ELSEIF ((DEST[31-0] GTRTHAN SRC/m32[31-0]) = TRUE)THEN
      ZF = 0;
      PF = 0;
      CF = 0;
  ELSEIF ((DEST[31-0] LESSTHAN SRC/m32[31-0]) = TRUE THEN
      ZF = 0;
      PF = 0;
      CF = 1;
  ELSE
      ZF = 1;
      PF = 0;
      CF = 0;
  FI

  CVTPI2PS
说明 | CVTPI2PS xmm, mm/m64
功能 | 32位整数转变为浮点数
算法 　DEST[31-0]　 = (float) (SRC/m64[31-0]);
  DEST[63-32]　= (float) (SRC/m64[63-32]);
  DEST[95-64]　= DEST[95-64];
  DEST[127-96] = DEST[127-96];

  CVTPS2PI
说明 | CVTPS2PI mm, xmm/m64
功能 | 低位的两个浮点数转变为整数
算法 　DEST[31-0]　= (int) (SRC/m64[31-0]);
  DEST[63-32] = (int) (SRC/m64[63-32]);

  CVTSI2SS
说明 | CVTSI2SS xmm, r/m32
功能 | 32位整数转变为浮点数，存入低位
算法 　DEST[31-0]　 = (float) (R/m32);
  DEST[63-32]　= DEST[63-32];
  DEST[95-64]　= DEST[95-64];
  DEST[127-96] = DEST[127-96];

CVTSS2SI
说明 | CVTSS2SI r32, xmm/m32
功能 | 低位的浮点数转变为32位整数
算法 | r32 = (int) (SRC/m32[31-0]);
  CVTTPS2PI
说明 | CVTTPS2PI mm, xmm/m64
功能 | 低位的两个浮点数转变为整数，并且舍位
算法 　DEST[31-0]　= (int) (SRC/m64[31-0]);
  DEST[63-32] = (int) (SRC/m64[63-32]);

CVTTSS2SI
说明 | CVTTSS2SI r32, xmm/ m32
功能 | 将最低位浮点数转换为整数，并舍位。
算法 | r32 = (INT) (SRC/m32[31-0]);
  DIVPS
说明 | DIVPS xmm1, xmm2/m128
功能 | 单精度数除法运算
算法 　DEST[31-0]　 = DEST[31-0]　 / (SRC/m128[31-0]);
  DEST[63-32]　= DEST[63-32]　/ (SRC/m128[63-32]);
  DEST[95-64]　= DEST[95-64]　/ (SRC/m128[95-64]);
  DEST[127-96] = DEST[127-96] / (SRC/m128[127-96]);

  DIVSS
说明 | DIVSS xmm1, xmm2/m32
功能 | 低位单精度数除法
算法 　DEST[31-0]　 = DEST[31-0] / (SRC/m32[31-0]);
  DEST[63-32]　= DEST[63-32];
  DEST[95-64]　= DEST[95-64];
  DEST[127-96] = DEST[127-96];

EMMS
说明 | EMMS
功能 | 将浮点标识字置空
算法 | FPUTagWord <- FFFF

FXRSTOR
说明 | FXRSTOR m512byte
功能 | 从m512byte中装入FP，MMX，以及SSE的状态
算法 | FP and MMX state and Streaming SIMD Extension state = m512byte;

FXSAVE
说明 | FXSAVE m512byte
功能 | 向m512byte中存入FP，MMX，以及SSE的状态
算法 | m512byte = FP and MMX state and Streaming SIMD Extension state;

LDMXCSR
说明 | LDMXCSR m32
功能 | 装入SSE的状态控制字
算法 | MXCSR = m32;

MAXPS
说明 | MAXPS xmm1, xmm2/m128
功能 | 返回最大值
算法 | IF (DEST[31-0]=NaN) THEN DEST[31-0] = SRC[31-0]; ELSEIF (SRC[31-0] = NaN) THEN DEST[31-0] = SRC[31-0]; ELSEIF (DEST[31-0] > SRC/m128[31-0]) THEN DEST[31-0] = DEST[31-0]; ELSE DEST[31-0] = SRC/m128[31-0]; FI IF (DEST[63-32]=NaN) THEN DEST[63-32] = SRC[63-32]; ELSEIF (SRC[63-32] = NaN) THEN DEST[63-32] = SRC[63-32]; ELSEIF (DEST[63-32] > SRC/m128[63-32]) THEN DEST[63-32] = DEST[63-32]; ELSE DEST[63-32] = SRC/m128[63-32]; FI IF (DEST[95-64]=NaN) THEN DEST[95-64] = SRC[95-64]; ELSEIF (SRC[95-64] = NaN) THEN DEST[95-64] = SRC[95-64]; ELSEIF (DEST[95-64] > SRC/m128[95-64]) THEN DEST[95-64] = DEST[95-64]; ELSE DEST[95-64] = SRC/m128[95-64]; FI IF (DEST[127-96]=NaN) THEN DEST[127-96] = SRC[127-96]; ELSEIF (SRC[127-96] = NaN) THEN DEST[127-96] = SRC[127-96]; ELSEIF (DEST[127-96] > SRC/m128[127-96]) THEN DEST[127-96] = DEST[127-96]; ELSE DEST[127-96] = SRC/m128[127-96]; FI

MAXSS
说明 | MAXSS xmm1, xmm2/m32
功能 | 返回低位最大值
算法 | 同上面类似，区别在于只对DEST[31-0]进行操作

MINPS
说明 | MINPS xmm1, xmm2/m128
功能 | 返回最小值
算法 | 略

MINSS
说明 | MINSS xmm1, xmm2/m32
功能 | 返回低位最小值
算法 | 略

MOVAPS
说明 | MOVAPS xmm1, xmm2/m128 或 MOVAPS xmm2/m128, xmm1
功能 | 对齐的数据传输指令
算法 | IF (destination = DEST) THEN IF (SRC = m128)THEN (* load instruction *) DEST[127-0] = m128; ELSE(* move instruction *) DEST[127=0] = SRC[127-0]; FI; ELSE IF (destination = m128) THEN (* store instruction *) m128 = SRC[127-0]; ELSE(* move instruction *) DEST[127-0] = SRC[127-0]; FI; FI;
  MOVHLPS
说明 | MOVHLPS xmm1, xmm2
功能 | 高位的两个数传向低位
算法 　DEST[127-64] = DEST[127-64];
  DEST[63-0]　 = SRC[127-64];

  MOVHPS
说明 | MOVHPS xmm, m64 或 MOVHPS m64, xmm
功能 | 高位数据传输指令
算法 　IF (destination = DEST) THEN(* load instruction *)
      DEST[127-64] = m64;
      DEST[31-0] = DEST[31-0];
      DEST[63-32] = DEST[63-32];
      ELSE (* store instruction *)
      m64 = SRC[127-64];
  FI;

  MOVLPS
说明 | MOVLPS xmm, m64 或 MOVLPS m64, xmm
功能 | 低位数据传输指令
算法 　IF (destination = DEST) THEN(* load instruction *)
      DEST[63-0] = m64;
      DEST[95-64] = DEST[95-64];
      DEST[127-96] = DEST[127-96];
  ELSE(* store instruction *)
      m64 = DEST[63-0];
  FI

  MOVLHPS
说明 | MOVLHPS xmm1, xmm2
功能 | 低位的两个数传向高位
算法 　DEST[127-64] = SRC[63-0];
  DEST[63-0]　 = DEST[63-0];

  MOVMSKPS
说明 | MOVMSKPS r32, xmm
功能 | 掩码移入32位寄存器
算法 　r32[0] 　　= SRC[31];
  r32[1] 　　= SRC[63];
  r32[2]　　 = SRC[95];
  r32[3]　　 = SRC[127];
  r32[7-4]　 = 0X0;
  r32[15-8]　= 0X00;
  r32[31-16] = 0X0000;

MOVNTPS
说明 | MOVNTPS m128, xmm
功能 | 将数据直接存入内存，减小对缓存的压力
算法 | Dm128 = SRC;
  MOVSS
说明 | MOVSS xmm1, xmm2/m32 或 MOVSS xmm2/m32, xmm1
功能 | 最低位数据的传输指令
算法 　IF (destination = DEST) THEN
      IF (SRC == m32) THEN(* load instruction *)
          DEST[31-0] = m32;
          DEST [63-32] = 0X00000000;
          DEST [95-64] = 0X00000000;
          DEST [127-96] = 0X00000000;
      ELSE(* move instruction *)
          DEST [31-0] = SRC[31-0];
          DEST [63-32] = DEST [63-32];
          DEST [95-64] = DEST [95-64];
          DEST [127-96] = DEST [127-96];
      FI
  ELSE
      IF (destination = m32) THEN(* store instruction *)
          m32 = SRC[31-0];
      ELSE (* move instruction *)
          DEST [31-0] = SRC[31-0];
          DEST [63-32] = DEST[63-32];
          DEST [95-64] = DEST [95-64];
          DEST [127-96] = DEST [127-96];
      FI
  FI

MOVUPS
说明 | MOVUPS xmm1, xmm2/m128 或 MOVUPS xmm2/m128, xmm1
功能 | 非对齐数据的传输指令
算法 | IF (destination = xmm) THEN IF (SRC = m128)THEN(* load instruction *) DEST[127-0] = m128; ELSE (* move instruction *) DEST[127-0] = SRC[127-0]; FI ELSE IF (destination = m128) THEN(* store instruction *) m128 = SRC[127-0]; ELSE (* move instruction *) DEST[127-0] = SRC[127-0]; FI FI
  MULPS
说明 | MULPS xmm1, xmm2/m128
功能 | 单精度数相乘
算法 　DEST[31-0]　 = DEST[31-0] * SRC/m128[31-0];
  DEST[63-32]　= DEST[63-32] * SRC/m128[63-32];
  DEST[95-64]　= DEST[95-64] * SRC/m128[95-64];
  DEST[127-96] = DEST[127-96] * SRC/m128[127-96];

  MULSS
说明 | MULSS xmm1, xmm2/m32
功能 | 最低位的单精度数相乘
算法 　DEST[31-0]　 = DEST[31-0] * SRC/m32[31-0];
  DEST[63-32]　= DEST[63-32];
  DEST[95-64]　= DEST[95-64];
  DEST[127-96] = DEST[127-96];

ORPS
说明 | ORPS xmm1, xmm2/m128
功能 | 求或运算
算法 | DEST[127-0] |= SRC/m128[127-0];
  RCPPS
说明 | RCPPS xmm1, xmm2/m128
功能 | 求倒数的近似值
算法 　DEST[31-0]　 = APPROX (1.0/(SRC/m128[31-0]));
  DEST[63-32]　= APPROX (1.0/(SRC/m128[63-32]));
  DEST[95-64]　= APPROX (1.0/(SRC/m128[95-64]));
  DEST[127-96] = APPROX (1.0/(SRC/m128[127-96]));

  RCPSS
说明 | RCPSS xmm1, xmm2/m32
功能 | 求最低位的倒数的近似值
算法 　DEST[31-0]　 = APPROX (1.0/(SRC/m32[31-0]));
  DEST[63-32]　= DEST[63-32];
  DEST[95-64]　= DEST[95-64];
  DEST[127-96] = DEST[127-96];

  RSQRTPS
说明 | RSQRTPS xmm1, xmm2/m128
功能 | 求倒数平方根的近似值
算法 　DEST[31-0]　 = APPROX (1.0/SQRT(SRC/m128[31-0]));
  DEST[63-32]　= APPROX (1.0/SQRT(SRC/m128[63-32]));
  DEST[95-64]　= APPROX (1.0/SQRT(SRC/m128[95-64]));
  DEST[127-96] = APPROX (1.0/SQRT(SRC/m128[127-96]));

  RSQRTSS
说明 | RSQRTSS xmm1, xmm2/m32
功能 | 求最低位倒数平方根的近似值
算法 　DEST[31-0]　 = APPROX (1.0/SQRT(SRC/m32[31-0]));
  DEST[63-32]　= DEST[63-32];
  DEST[95-64]　= DEST[95-64];
  DEST[127-96] = DEST[127-96];

SHUFPS
说明 | SHUFPS xmm1, xmm2/m128, imm8
功能 | 打乱顺序
算法 | FP_SELECT = (imm8 >> 0) AND 0X3; IF (FP_SELECT = 0) THEN DEST[31-0] = DEST[31-0]; ELSEIF (FP_SELECT = 1) THEN DEST[31-0] = DEST[63-32]; ELSEIF (FP_SELECT = 2) THEN DEST[31-0] = DEST[95-64]; ELSE DEST[31-0] = DEST[127-96]; FI FP_SELECT = (imm8 >> 2) AND 0X3; IF (FP_SELECT = 0) THEN DEST[63-32] = DEST[31-0]; ELSEIF (FP_SELECT = 1) THEN DEST[63-32] = DEST[63-32]; ELSEIF (FP_SELECT = 2) THEN DEST[63-32] = DEST[95-64]; ELSE DEST[63-32] = DEST[127-96]; FI FP_SELECT = (imm8 >> 4) AND 0X3; IF (FP_SELECT = 0) THEN DEST[95-64] = SRC/m128[31-0]; ELSEIF (FP_SELECT = 1) THEN DEST[95-64] = SRC/m128 [63-32]; ELSEIF (FP_SELECT = 2) THEN DEST[95-64] = SRC/m128 [95-64]; ELSE DEST[95-64] = SRC/m128 [127-96]; FI FP_SELECT = (imm8 >> 6) AND 0X3; IF (FP_SELECT = 0) THEN DEST[127-96] = SRC/m128 [31-0]; ELSEIF (FP_SELECT = 1) THEN DEST[127-96] = SRC/m128 [63-32]; ELSEIF (FP_SELECT = 2) THEN DEST[127-96] = SRC/m128 [95-64]; ELSE DEST[127-96] = SRC/m128 [127-96]; FI
  SQRTPS
说明 | SQRTPS xmm1, xmm2/m128
功能 | 求平方根
算法 　DEST[31-0]　 = SQRT (SRC/m128[31-0] );
  DEST[63-32]　= SQRT (SRC/m128[63-32]);
  DEST[95-64]　= SQRT (SRC/m128[95-64]);
  DEST[127-96] = SQRT (SRC/m128[127-96]);

STMXCSR
说明 | STMXCSR m32
功能 | 存储SSE控制字
算法 | m32 = MXCSR;
  SUBPS
说明 | SUBPS xmm1, xmm2/m128
功能 | 单精度数的减法运算
算法 　DEST[31-0]　 = DEST[31-0] - SRC/m128[31-0];
  DEST[63-32]　= DEST[63-32] - SRC/m128[63-32];
  DEST[95-64]　= DEST[95-64] - SRC/m128[95-64];
  DEST[127-96] = DEST[127-96] - SRC/m128[127-96];

  SUBSS
说明 | SUBSS xmm1, xmm2/m32
功能 | 最低位数相减
算法 　DEST[31-0]　 = DEST[31-0] - SRC/m32[31-0];
  DEST[63-32]　= DEST[63-32];
  DEST[95-64]　= DEST[95-64];
  DEST[127-96] = DEST[127-96];

  UCOMISS
说明 | UCOMISS xmm1, xmm2/m32
功能 | 比较低位数并且设置标志位
算法 　OF = 0;
  SF = 0;
  AF = 0;
  IF ((DEST[31-0] UNORD SRC/m32[31-0]) = TRUE) THEN
      ZF = 1;
      PF = 1;
      CF = 1;
  ELSEIF ((DEST[31-0] GTRTHAN SRC/m32[31-0]) = TRUE)THEN
      ZF = 0;
      PF = 0;
      CF = 0;
  ELSEIF ((DEST[31-0] LESSTHAN SRC/m32[31-0]) = TRUE THEN
      ZF = 0;
      PF = 0;
      CF = 1;
  ELSE
      ZF = 1;
      PF = 0;
      CF = 0;
  FI

  UNPCKHPS
说明 | UNPCKHPS xmm1, xmm2/m128
功能 | 高位两数交替传输
算法 　DEST[31-0]　 = DEST[95-64];
  DEST[63-32]　= SRC/m128[95-64];
  DEST[95-64]　= DEST[127-96];
  DEST[127-96] = SRC/m128[127-96];

  UNPCKLPS
说明 | UNPCKLPS xmm1, xmm2/m128
功能 | 低位两数交替传输
算法 　DEST[31-0]　 = DEST[31-0];
  DEST[63-32]　= SRC/m128[31-0];
  DEST[95-64]　= DEST[63-32];
  DEST[127-96] = SRC/m128[63-32];

XORPS
说明 | XORPS xmm1, xmm2/m128
功能 | 异或运算
算法 | DEST[127-0] = DEST/m128[127-0] XOR SRC/m128[127-0]


基于MMX指令集的程序设计简介

作者：Alex Farber

基于MMX指令集的程序设计简介

MMX技术简介

    Intel 公司的MMX™（多媒体增强指令集）技术可以大大提高应用程序对二维三维图形和图象的处理能力。Intel MMX技术可用于对大量数据和复杂数组进行的复杂处理，使用MMX技术可处理的数据基本单位可以是字节（byte）、字（word），或者是双字（double-word）。

    Visual Studio .NET 2003提供了对MMX指令集特性的支持，从而可以不必编写汇编代码，直接使用C++代码就可以实现MMX指令的功能。通过参考Intel软件说明书（Intel Software manuals）[1]以及阅读MSDN中有关MMX编程技术的主题会使你更好地把握MMX编程的要点。

    MMX技术实现了单道指令多道数据流（SIMD，single-instruction, multiple-data）的执行模式。考虑下面一个需要编程完成的任务，在一个字节（BYTE）数组中使其中每一个元素加上一个数，在传统的程序中，实现这个功能的算法如下：

    for each b in array //对数组中的每一个元素b
    b = b + n //加上一个数n

下面看看它的实现细节：

    for each b in array //对数组中的每一个元素b
    {
        把b加载到寄存器中
        把此寄存器中的数加上n
        把所得寄存器中的结果放回内存
    }

    具有MMX指令集支持的处理器有八个64位的寄存器，每一个寄存器可以存放8个字节（byte）、4个字(word)或2个双字(double-word)。MMX技术同时提供了一个MMX指令集，其中的指令可以可以把一个数值（其类型可以是字节、字或双字）加载到这些MMX寄存器中，在寄存器中进行算术或逻辑运算，然后把寄存器中的结果放回内存存储单元。上面的例子采用MMX技术后的算法是这样的：

    for each 8 members in array //把数组中的8个字节（其中一个字节为数组中的一个单位）作为一组取出
    {
        把这8个字节加载到MMX寄存器中
        通过一个CPU指令执行周期把这个寄存器中的8个字节都加上n
        把寄存器中计算的结果写回内存
    }

    C++编程人员不必直接使用MMX指令集中的指令访问这些MMX寄存器。你可以使用64位的数据类型__m64和一系列C++函数来进行相关的算术和逻辑运算。而决定程序使用哪个MMX寄存器以及代码优化是C++编译器的任务。

    Visual C++ MMXSwarm [4]是MSDN中提供的一个很好的使用MMX技术进行图象处理的例子，它包含了一些封装好了的类简化了使用MMX技术的操作，并向你展示了对各种不同格式图象进行处理的操作（如单色24位象素RGB、32位象素RGB等）。本文只是对使用Visual C++实现MMX程序设计的简单介绍。如果你感兴趣的话，可以参看MSDN上MMXSwarm的例子。

MMX程序设计详细介绍

包含的头文件

所有的MMX指令集函数在emmintrin.h文件中定义：
    #include <emmintrin.h>

因为程序中用到的MMX处理器指令是由编译器决定，所以它并没有相关的.lib库文件。

    __m64 数据类型

这种类型的变量可用作MMX指令的操作数，它不能被直接访问。_m64类型的变量被自动分配为8个字节的字长。

CPU对MMX指令集的支持

    如果你的CPU能够具有了MMX指令集，你就可以使用Visual Studio .NET 2003提供的对MMX指令集支持的C++函数库了，你可以查看MSDN中的一个Visual C++ CPUID[3]的例子，它可以帮你检测你的CPU是否支持SSE、MMX指令集或其它的CPU功能。

饱和算法（Saturation Arithmetic）和封装模式（Wraparound Mode）

    MMX技术支持一种叫做saturating arithmetic（饱和算法）的计算模式。在饱和模式下，当计算结果发生溢出（上溢或下溢）时，CPU会自动去掉溢出的部分，使计算结果取该数据类型表示数值的上限值（如果上溢）或下限值（如果下溢）。饱和模式的计算用于对图象的处理。
下面的例子能够让你理解饱和模式和封装模式的区别。如果一个字节(BYTE)类型变量的值为255，然后将其值加一。在封装模式下，相加结果为0（去掉进位）；在饱和模式下，结果为255。饱和模式用类似的方法来处理下溢出，比如对于一个字节数据类型的数在饱和模式下，1减2的结果为0（而不是-1）。每一个MMX算术指令都有这两种模式：饱和模式和封装模式。本文所要讨论的项目只使用饱和模式下的MMX指令。

编程实例

    以下讲解了MMX技术在Visual Studio .NET 2003下的应用实例，你可以在http://www.codeproject.com/cpp/mmxintro/MMX_src.zip下载示例程序压缩包。该压缩包中含有两个项目，这两个项目是基于微软基本类库（MFC）建立的Visual C++.NET项目，你也可以按照下面的讲解建立这两个项目。

MMX8 演示项目

    MMX8是一个单文档界面（SDI）的应用程序，用来对每象素8位的单色位图进行简单处理。源图象和处理后的图象会在窗体中显示出来。新建的ATL（活动模版库）类 Cimage用来从资源中提取图象并在窗体中显示出来。程序要对图象进行两种处理操作：图象颜色反相和改变图象的亮度。每一种处理操作可以用下面几种方法之中其中的一种来实现：

纯C++代码；
    使用C++的MMX功能函数的代码；
    使用MMX汇编指令的代码。

对图象进行处理计算的时间会显示在状态栏中。

用纯C++实现的图象颜色反相函数：

    void CImg8Operations::InvertImageCPlusPlus( BYTE* pSource, BYTE* pDest, int nNumberOfPixels )
    {
        for ( int i = 0; i < nNumberOfPixels; i++ )
        {
            *pDest++ = 255 - *pSource++;
        }
    }

    为了查询使用C++ MMX指令函数的方法，需要参考Intel软件说明书（Intel Software manuals）中有关MMX汇编指令的说明，首先我是在第一卷的第八章找到了MMX相关指令的大体介绍，然后在第二卷找到了有关这些MMX指令的详细说明，这些说明有一部分涉及了与其特性相关的C++函数。然后我通过这些MMX指令对应的C++函数查找了MSDN中与其相关的说明。在MMX8示例程序中用到的MMX指令和相关的C++函数见下表：

实现的功能 | 对应的MMX汇编指令 | Visual C++.NET中的MMX函数
清除MMX寄存器中的内容，即初始化（以避免和浮点数操作发生冲突）。 | emms | _mm_empty
将两个64位数中对应的（8个）无符号（8位）字节同时进行减法操作。 | psubusb | _mm_subs_pu8
将两个64位数中对应的（8个）无符号（8位）字节同时进行加法操作。 | paddusb | _mm_adds_pu8

用Visual C++.NET的MMX指令函数实现图象颜色反相的函数：

void CImg8Operations::InvertImageC_MMX( BYTE* pSource, BYTE* pDest, int nNumberOfPixels )
{
__int64 i = 0;
i = ~i; // 0xffffffffffffffff

// 每次循环处理8个象素
int nLoop = nNumberOfPixels/8;

__m64* pIn = (__m64*) pSource; // 输入的字节数组指针
__m64* pOut = (__m64*) pDest; // 输出的字节数组指针

__m64 tmp; // 临时工作变量

_mm_empty(); // 执行MMX指令：emms，初始化MMX寄存器

__m64 n1 = Get_m64(i);

for ( int i = 0; i < nLoop; i++ )
{
tmp = _mm_subs_pu8 (n1 , *pIn); // 饱和模式下的无符号减法
//对每一个字节执行操作：tmp = n1 - *pIn
*pOut = tmp;

pIn++; // 取下面的8个象素点
pOut++;
}

_mm_empty(); // 执行MMX指令：emms，清除MMX寄存器中的内容
}

__m64 CImg8Operations::Get_m64(__int64 n)
{
union __m64__m64
{
__m64 m;
__int64 i;
} mi;

mi.i = n;
return mi.m;
}

虽然这个函数在非常短的时间就执行完成了，但我记录了这3种方法需要的时间，以下是在我的计算机上运行的结果：

纯C++代码 43毫秒
使用C++的MMX指令函数的代码 26毫秒
使用MMX汇编指令的代码 26毫秒

上面的图象处理时间必须在程序Release优化编译后执行时才能体现出很好的效果。

而改变图象的亮度我采用了最简单的方法：对图象中的每一个象素的颜色值进行加减运算。相对前面的处理函数而言，这样的转换函数有些复杂，因为我们需要把处理过程分成两种情况，一种是增加象素颜色值，另一种是减少象素颜色值。

用纯C++函数实现的改变图象亮度的函数：

void CImg8Operations::ChangeBrightnessCPlusPlus( BYTE* pSource, BYTE* pDest, int nNumberOfPixels,
     int nChange )
{
if ( nChange > 255 )
nChange = 255;
else if ( nChange < -255 )
nChange = -255;

BYTE b = (BYTE) abs(nChange);

int i, n;

if ( nChange > 0 ) //增加象素颜色值
{
for ( i = 0; i < nNumberOfPixels; i++ )
{
n = (int)(*pSource++ + b);

if ( n > 255 )
n = 255;

*pDest++ = (BYTE) n;
}
}
else //减少象素颜色值
{
for ( i = 0; i < nNumberOfPixels; i++ )
{
n = (int)(*pSource++ - b);

if ( n < 0 )
n = 0;
*pDest++ = (BYTE) n;
}
}
}

用Visual C++.NET的MMX指令函数实现的改变图象亮度函数：

void CImg8Operations::ChangeBrightnessC_MMX( BYTE* pSource, BYTE* pDest, int nNumberOfPixels,
     int nChange )
{
if ( nChange > 255 )
nChange = 255;
else if ( nChange < -255 )
nChange = -255;

BYTE b = (BYTE) abs(nChange);

__int64 c = b;

for ( int i = 1; i <= 7; i++ )
{
c = c << 8;
c |= b;
}

// 在一次循环中处理8个象素
int nNumberOfLoops = nNumberOfPixels / 8;

__m64* pIn = (__m64*) pSource; // 输入的字节数组
__m64* pOut = (__m64*) pDest; // 输出的字节数组

__m64 tmp; // 临时工作变量

_mm_empty(); // 执行MMX指令：emms

__m64 nChange64 = Get_m64(c);

if ( nChange > 0 )
{
for ( i = 0; i < nNumberOfLoops; i++ )
{
tmp = _mm_adds_pu8(*pIn, nChange64); // 饱和模式下的无符号加法
// 对每一个字节执行操作：tmp = *pIn + nChange64

*pOut = tmp;

pIn++; // 取下面8个象素
pOut++;
}
}
else
{
for ( i = 0; i < nNumberOfLoops; i++ )
{
tmp = _mm_subs_pu8(*pIn, nChange64); // 饱和模式下的无符号减法
// 对每一个字节执行操作：tmp = *pIn - nChange64

*pOut = tmp;

pIn++; //取下面8个象素
pOut++;
}
}

_mm_empty(); // 执行MMX指令：emms
}

注意参数nChange的符号每次调用函数时在循环体外只检查一次，而不是放在循环体内，那样会被检查成千上万次。下面是在我的计算机上处理图象花费的时间：

纯C++代码 49毫秒
使用C++的MMX指令函数的代码 26毫秒
使用MMX汇编指令的代码 26毫秒

MMX32 演示项目

MMX32项目可对32位象素的RGB图象进行处理。进行的图象处理工作是图象颜色反相操作和更改图象颜色的平衡度（将象素点的每一种颜色乘以一定的值）操作。

MMX的乘法实现起来比加减法复杂得多，因为乘法运算通常得出的结果的位数不再是以前位数的大小。比如，如果乘法的操作数有一个字节（8位的BYTE）大小，那么结果会达到一个字（16位的WORD）大小。这需要额外的转换，并且使用MMX汇编指令和C++代码进行图象转换花费时间的差别不是很大（时间差为5-10%）。

用Visual C++.NET的MMX指令函数实现的更改图象颜色平衡度的函数：

void CImg32Operations::ColorsC_MMX( BYTE* pSource, BYTE* pDest, int nNumberOfPixels, float fRedCoefficient,
     float fGreenCoefficient, float fBlueCoefficient )
{
int nRed = (int)(fRedCoefficient * 256.0f);
int nGreen = (int)(fGreenCoefficient * 256.0f);
int nBlue = (int)(fBlueCoefficient * 256.0f);

// 设置相乘系数
__int64 c = 0;
c = nRed;
c = c << 16;
c |= nGreen;
c = c << 16;
c |= nBlue;

__m64 nNull = _m_from_int(0); // null
__m64 tmp = _m_from_int(0); // 临时工作临时变量初始化

_mm_empty(); // 清空MMX寄存器。

__m64 nCoeff = Get_m64(c);

DWORD* pIn = (DWORD*) pSource; // 输入双字数组
DWORD* pOut = (DWORD*) pDest; // 输出双字数组

for ( int i = 0; i < nNumberOfPixels; i++ )
{
tmp = _m_from_int(*pIn); // tmp = *pIn (在tmp的低32位写入数据)

tmp = _mm_unpacklo_pi8(tmp, nNull ); //将tmp中低位的4个字节转化为字
//字的高位用nNull中对应位上的位值填充。

tmp = _mm_mullo_pi16 (tmp , nCoeff); //将tmp中的每一个字相乘，将相乘结果的高位送到nCoeff，在tmp中只保留每个结果的低位。

tmp = _mm_srli_pi16 (tmp , 8); // 将tmp中的每一个字右移8位，相当于除以256

tmp = _mm_packs_pu16 (tmp, nNull); // 使用饱和模式将tmp中的结果做如下处理：
//将tmp中的4个字转化为4个字节，并将这4个字节写到tmp中的低32位中
// 同时，将nNull中的4个字转化为4个字节，并将这4个字节写到tmp的高32位中。

*pOut = _m_to_int(tmp); // *pOut = tmp (将tmp低32位的数据放入pOut数组中)

pIn++;
pOut++;
}

_mm_empty();
}

你可以参看示例项目的源代码了解有关此项目的更多的细节。

SSE2 技术

SSE2技术包含有一个类似MMX中对整数操作的指令集，同时也包含128位的SSE寄存器组。比如，用SSE2技术实现更改图象颜色平衡度能够比用纯C++代码实现此功能在效率上有很大提升。SSE2同时是SSE技术的扩展，比如它不仅可以单精度浮点数数组，而且能够处理双精度浮点数数据类型的数组。用C++实现的MMXSwarm
     示例项目不仅使用了MMX指令函数，而且使用了SSE2指令对整型数操作的函数。

参考文档：

[1] Intel软件说明书（Intel Software manuals）：http://developer.intel.com/design/archives/processors/mmx/index.htm
     。

[2] MSDN中有关MMX技术的主题：http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/vcrefsupportformmxtechnology.asp。

[3] Microsoft Visual C++ CPUID项目示例：http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vcsample/html/vcsamcpuiddeterminecpucapabilities.asp。

[4] Microsoft Visual C++ MMXSwarm项目示例：
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vcsample/html/vcsamMMXSwarmSampleDemonstratesCImageVisualCsMMXSupport.asp。

[5]
Matt Pietrek在Microsoft Systems Journal 1998年2月刊上的评论文章：
http://www.microsoft.com/msj/0298/hood0298.aspx 。


基于SSE指令集的程序设计简介

作者：Alex Farber

基于SSE指令集的程序设计简介

SSE技术简介

    Intel公司的单指令多数据流式扩展（SSE，Streaming SIMD Extensions）技术能够有效增强CPU浮点运算的能力。Visual Studio .NET 2003提供了对SSE指令集的编程支持，从而允许用户在C++代码中不用编写汇编代码就可直接使用SSE指令的功能。MSDN中有关SSE技术的主题[1]有可能会使不熟悉使用SSE汇编指令编程的初学者感到困惑，但是在阅读MSDN有关文档的同时，参考一下Intel软件说明书（Intel Software manuals）[2]会使你更清楚地理解使用SSE指令编程的要点。

    SIMD（single-instruction, multiple-data）是一种使用单道指令处理多道数据流的CPU执行模式，即在一个CPU指令执行周期内用一道指令完成处理多个数据的操作。考虑一下下面这个任务：计算一个很长的浮点型数组中每一个元素的平方根。实现这个任务的算法可以这样写：

 for each f in array //对数组中的每一个元素
f = sqrt(f) //计算它的平方根

为了了解实现的细节，我们把上面的代码这样写：

 for each f in array
{
把f从内存加载到浮点寄存器
计算平方根
再把计算结果从寄存器中取出放入内存
}

具有Intel SSE指令集支持的处理器有8个128位的寄存器，每一个寄存器可以存放4个（32位）单精度的浮点数。SSE同时提供了一个指令集，其中的指令可以允许把浮点数加载到这些128位的寄存器之中，这些数就可以在这些寄存器中进行算术逻辑运算，然后把结果放回内存。采用SSE技术后，算法可以写成下面的样子：

 for each 4 members in array //对数组中的每4个元素
{
把数组中的这4个数加载到一个128位的SSE寄存器中
在一个CPU指令执行周期中完成计算这4个数的平方根的操作
把所得的4个结果取出写入内存
}

C++编程人员在使用SSE指令函数编程时不必关心这些128位的寄存器，你可以使用128位的数据类型“__m128”和一系列C++函数来实现这些算术和逻辑操作，而决定程序使用哪个SSE寄存器以及代码优化是C++编译器的任务。当需要对很长的浮点数数组中的元素进行处理的时候，SSE技术确实是一种很高效的方法。

SSE程序设计详细介绍

包含的头文件：

所有的SSE指令函数和__m128数据类型都在xmmintrin.h文件中定义：

 #include <xmmintrin.h>

因为程序中用到的SSE处理器指令是由编译器决定，所以它并没有相关的.lib库文件。

  数据分组（Data Alignment）

    由SSE指令处理的每一个浮点数数组必须把其中需要处理的数每16个字节（128位二进制）分为一组。一个静态数组（static array）可由__declspec(align(16))关键字声明：

 __declspec(align(16)) float m_fArray[ARRAY_SIZE];

动态数组（dynamic array）可由_aligned_malloc函数为其分配空间：

 m_fArray = (float*) _aligned_malloc(ARRAY_SIZE * sizeof(float), 16);

由_aligned_malloc函数分配空间的动态数组可以由_aligned_free函数释放其占用的空间：

 _aligned_free(m_fArray);

  __m128 数据类型

    该数据类型的变量可用做SSE指令的操作数，它们不能被用户指令直接存取。_m128类型的变量被自动分配为16个字节的字长。

  CPU对SSE指令集的支持

    如果你的CPU能够具有了SSE指令集，你就可以使用Visual Studio .NET 2003提供的对SSE指令集支持的C++函数库了，你可以查看MSDN中的一个Visual C++ CPUID的例子[4]，它可以帮你检测你的CPU是否支持SSE、MMX指令集或其它的CPU功能。

  编程实例

    以下讲解了SSE技术在Visual Studio .NET 2003下的应用实例，你可以在http://www.codeproject.com/cpp/sseintro/SSE_src.zip下载示例程序压缩包。该压缩包中含有两个项目，这两个项目是基于微软基本类库（MFC）建立的Visual C++.NET项目，你也可以按照下面的讲解建立这两个项目。

  SSETest 示例项目

SSETest项目是一个基于对话框的应用程序，它用到了三个浮点数组参与运算：

 fResult[i] = sqrt( fSource1[i]*fSource1[i] + fSource2[i]*fSource2[i] ) + 0.5

其中i = 0, 1, 2 ... ARRAY_SIZE-1

其中ARRAY_SIZE被定义为30000。数据源数组（Source数组）通过使用sin和cos函数给它赋值，我们用Kris Jearakul开发的瀑布状图表控件（Waterfall chart control）[3] 来显示参与计算的源数组和结果数组。计算所需的时间(以毫秒ms为单位)在对话框中显示出来。我们使用三种不同的途径来完成计算：

纯C++代码；
使用SSE指令函数的C++代码；
包含SSE汇编指令的代码。

  纯C++代码：

 void CSSETestDlg::ComputeArrayCPlusPlus(
float* pArray1, // [输入] 源数组1
float* pArray2, // [输入] 源数组2
float* pResult, // [输出] 用来存放结果的数组
int nSize) // [输入] 数组的大小
{

int i;

float* pSource1 = pArray1;
float* pSource2 = pArray2;
float* pDest = pResult;

for ( i = 0; i < nSize; i++ )
{
*pDest = (float)sqrt((*pSource1) * (*pSource1) + (*pSource2) * (*pSource2)) + 0.5f;

pSource1++;
pSource2++;
pDest++;
}
}

    下面我们用具有SSE特性的C++代码重写上面这个函数。为了查询使用SSE指令C++函数的方法，我参考了Intel软件说明书（Intel Software manuals）中有关SSE汇编指令的说明，首先我是在第一卷的第九章找到的相关SSE指令，然后在第二卷找到了这些SSE指令的详细说明，这些说明有一部分涉及了与其特性相关的C++函数。然后我通过这些SSE指令对应的C++函数查找了MSDN中与其相关的说明。搜索的结果见下表：

实现的功能 | 对应的SSE汇编指令 | Visual C++.NET中的SSE函数
将4个32位浮点数放进一个128位的存储单元。 | movss 和 shufps | _mm_set_ps1
将4对32位浮点数同时进行相乘操作。这4对32位浮点数来自两个128位的存储单元，再把计算结果（乘积）赋给一个128位的存储单元。 | mulps | _mm_mul_ps
将4对32位浮点数同时进行相加操作。这4对32位浮点数来自两个128位的存储单元，再把计算结果（相加之和）赋给一个128位的存储单元。 | addps | _mm_add_ps
对一个128位存储单元中的4个32位浮点数同时进行求平方根操作。 | sqrtps | _mm_sqrt_ps

  使用Visual C++.NET的 SSE指令函数的代码：

 void CSSETestDlg::ComputeArrayCPlusPlusSSE(
float* pArray1, // [输入] 源数组1
float* pArray2, // [输入] 源数组2
float* pResult, // [输出] 用来存放结果的数组
int nSize) // [输入] 数组的大小
{
int nLoop = nSize/ 4;

__m128 m1, m2, m3, m4;

__m128* pSrc1 = (__m128*) pArray1;
__m128* pSrc2 = (__m128*) pArray2;
__m128* pDest = (__m128*) pResult;

__m128 m0_5 = _mm_set_ps1(0.5f); // m0_5[0, 1, 2, 3] = 0.5

for ( int i = 0; i < nLoop; i++ )
{
m1 = _mm_mul_ps(*pSrc1, *pSrc1); // m1 = *pSrc1 * *pSrc1
m2 = _mm_mul_ps(*pSrc2, *pSrc2); // m2 = *pSrc2 * *pSrc2
m3 = _mm_add_ps(m1, m2); // m3 = m1 + m2
m4 = _mm_sqrt_ps(m3); // m4 = sqrt(m3)
*pDest = _mm_add_ps(m4, m0_5); // *pDest = m4 + 0.5

pSrc1++;
pSrc2++;
pDest++;
}
}

  使用SSE汇编指令实现的C++函数代码：

 void CSSETestDlg::ComputeArrayAssemblySSE(
float* pArray1, // [输入] 源数组1
float* pArray2, // [输入] 源数组2
float* pResult, // [输出] 用来存放结果的数组
int nSize) // [输入] 数组的大小
{
int nLoop = nSize/4;
float f = 0.5f;

_asm
{
movss xmm2, f // xmm2[0] = 0.5
shufps xmm2, xmm2, 0 // xmm2[1, 2, 3] = xmm2[0]

mov esi, pArray1 // 输入的源数组1的地址送往esi
mov edx, pArray2 // 输入的源数组2的地址送往edx

mov edi, pResult // 输出结果数组的地址保存在edi
mov ecx, nLoop //循环次数送往ecx

start_loop:
movaps xmm0, [esi] // xmm0 = [esi]
mulps xmm0, xmm0 // xmm0 = xmm0 * xmm0

movaps xmm1, [edx] // xmm1 = [edx]
mulps xmm1, xmm1 // xmm1 = xmm1 * xmm1

addps xmm0, xmm1 // xmm0 = xmm0 + xmm1
sqrtps xmm0, xmm0 // xmm0 = sqrt(xmm0)

addps xmm0, xmm2 // xmm0 = xmm1 + xmm2

 movaps [edi], xmm0 // [edi] = xmm0

add esi, 16 // esi += 16
add edx, 16 // edx += 16
add edi, 16 // edi += 16

dec ecx // ecx--
jnz start_loop //如果不为0则转向start_loop
}
}

最后，在我的计算机上运行计算测试的结果：

纯C++代码计算所用的时间是26 毫秒
使用SSE的C++ 函数计算所用的时间是 9 毫秒
包含SSE汇编指令的C++代码计算所用的时间是 9 毫秒

以上的时间结果是在Release优化编译后执行程序得出的。

SSESample 示例项目

SSESample项目是一个基于对话框的应用程序，其中它用下面的浮点数数组进行计算：

 fResult[i] = sqrt(fSource[i]*2.8)

其中i = 0, 1, 2 ... ARRAY_SIZE-1

这个程序同时计算了数组中的最大值和最小值。ARRAY_SIZE被定义为100000，数组中的计算结果在列表框中显示出来。其中在我的机子上用下面三种方法计算所需的时间是：
 纯C++代码计算 6 毫秒
使用SSE的C++ 函数计算 3 毫秒
使用SSE汇编指令计算 2 毫秒

大家看到，使用SSE汇编指令计算的结果会好一些，因为使用了效率增强了的SSX寄存器组。但是在通常情况下，使用SSE的C++ 函数计算会比汇编代码计算的效率更高一些，因为C++编译器的优化后的代码有很高的运算效率，若要使汇编代码比优化后的代码运算效率更高，这通常是很难做到的。

  纯C++代码：

 // 输入: m_fInitialArray
// 输出: m_fResultArray, m_fMin, m_fMax
void CSSESampleDlg::OnBnClickedButtonCplusplus()
{
m_fMin = FLT_MAX;
m_fMax = FLT_MIN;

int i;

for ( i = 0; i < ARRAY_SIZE; i++ )
{
m_fResultArray[i] = sqrt(m_fInitialArray[i] * 2.8f);

if ( m_fResultArray[i] < m_fMin )
m_fMin = m_fResultArray[i];

if ( m_fResultArray[i] > m_fMax )
m_fMax = m_fResultArray[i];
}
}

  使用Visual C++.NET的 SSE指令函数的代码：

 // 输入: m_fInitialArray
// 输出: m_fResultArray, m_fMin, m_fMax
void CSSESampleDlg::OnBnClickedButtonSseC()
{
__m128 coeff = _mm_set_ps1(2.8f); // coeff[0, 1, 2, 3] = 2.8
__m128 tmp;

__m128 min128 = _mm_set_ps1(FLT_MAX); // min128[0, 1, 2, 3] = FLT_MAX
__m128 max128 = _mm_set_ps1(FLT_MIN); // max128[0, 1, 2, 3] = FLT_MIN

__m128* pSource = (__m128*) m_fInitialArray;
__m128* pDest = (__m128*) m_fResultArray;

for ( int i = 0; i < ARRAY_SIZE/4; i++ )
{
tmp = _mm_mul_ps(*pSource, coeff); // tmp = *pSource * coeff
*pDest = _mm_sqrt_ps(tmp); // *pDest = sqrt(tmp)

min128 = _mm_min_ps(*pDest, min128);
max128 = _mm_max_ps(*pDest, max128);

pSource++;
pDest++;
}

 // 计算max128的最大值和min128的最小值
union u
{
__m128 m;
float f[4];
} x;

x.m = min128;
m_fMin = min(x.f[0], min(x.f[1], min(x.f[2], x.f[3])));

x.m = max128;
m_fMax = max(x.f[0], max(x.f[1], max(x.f[2], x.f[3])));
}

  使用SSE汇编指令的C++函数代码：

 // 输入: m_fInitialArray
// 输出: m_fResultArray, m_fMin, m_fMax
void CSSESampleDlg::OnBnClickedButtonSseAssembly()
{

float* pIn = m_fInitialArray;
float* pOut = m_fResultArray;

float f = 2.8f;
float flt_min = FLT_MIN;
float flt_max = FLT_MAX;

__m128 min128;
__m128 max128;

// 使用以下的附加寄存器:xmm2、xmm3、xmm4:
// xmm2 – 相乘系数
// xmm3 – 最小值
// xmm4 – 最大值

_asm
{
movss xmm2, f // xmm2[0] = 2.8
shufps xmm2, xmm2, 0 // xmm2[1, 2, 3] = xmm2[0]

movss xmm3, flt_max // xmm3 = FLT_MAX
shufps xmm3, xmm3, 0 // xmm3[1, 2, 3] = xmm3[0]

movss xmm4, flt_min // xmm4 = FLT_MIN
shufps xmm4, xmm4, 0 // xmm3[1, 2, 3] = xmm3[0]

mov esi, pIn // 输入数组的地址送往esi
mov edi, pOut // 输出数组的地址送往edi
mov ecx, ARRAY_SIZE/4 // 循环计数器初始化

start_loop:
movaps xmm1, [esi] // xmm1 = [esi]
mulps xmm1, xmm2 // xmm1 = xmm1 * xmm2
sqrtps xmm1, xmm1 // xmm1 = sqrt(xmm1)
movaps [edi], xmm1 // [edi] = xmm1

minps xmm3, xmm1
maxps xmm4, xmm1

add esi, 16
add edi, 16

dec ecx
jnz start_loop

movaps min128, xmm3
movaps max128, xmm4
}

union u
{
__m128 m;
float f[4];
} x;

x.m = min128;
m_fMin = min(x.f[0], min(x.f[1], min(x.f[2], x.f[3])));

x.m = max128;
m_fMax = max(x.f[0], max(x.f[1], max(x.f[2], x.f[3])));
}

参考文档：

[1]MSDN, SSE技术主题：
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/vcrefstreamingsimdextensions.asp

[2]Intel软件说明书（Intel Software manuals）：
http://developer.intel.com/design/archives/processors/mmx/index.htm

[3] Kris Jearakul的瀑布状图表控件：http://www.codeguru.com/controls/Waterfall.shtml

[4] Microsoft Visual C++ CPUID示例：
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vcsample/html/vcsamcpuiddeterminecpucapabilities.asp

[5] Matt Pietrek在Microsoft Systems Journal 1998年2月刊上的评论文章：
http://www.microsoft.com/msj/0298/hood0298.aspx 。