ESC
科技 2 分钟阅读

90年代的SIMD:为英特尔的奔腾MMX编程

本文回顾了20世纪90年代英特尔奔腾MMX处理器上的SIMD技术。从Cray-1向量处理器讲起,介绍MMX如何复用x87浮点寄存器实现64位SIMD操作,支持饱和算术、并行移位与乘法,常用于图形优化。文章还讨论了MMX与3DNow!、SSE的竞争,以及游戏软件渲染器对MMX的实际应用。

来源:Hacker News

SIMD操作从根本上与寄存器宽度相关。

1976年的Cray-1以“向量处理器”(vector processor)著称。它有八个64元素向量寄存器,每个寄存器保存64位值。

与我们看到的SIMD类似,Cray-1不需要64条单独的ADD指令。那么,既然这显然是SIMD式的,为什么我们不直接称它为SIMD呢?因为存在架构上的区别。SIMD在固定宽度(fixed-width)的寄存器上操作,而Cray-1的向量寄存器更像是一个“序列容器”。

向量处理器通常以存在SET VECTOR SIZE指令为特征。这个向量功能单元可以在多个周期内处理元素。

Cray-1有一个**VL(向量长度)**寄存器,以及能够设置该长度的指令。向量功能单元随后在多个周期内处理那么多元素。数组中其余多余的元素根本没有被使用/循环。

Cray-1的VL寄存器是7位,尽管该架构实际上只使用到64(2⁶)以内的值,因为Cray-1向量寄存器最多包含64个元素。另外,一个值得一提的小细节是:Cray-1将VL=0解释为64,即处理向量寄存器中的所有64个元素。

正如我们之前看到的,一个巧妙的设计决定是英特尔没有添加全新的寄存器文件。相反,MMX复用了浮点栈寄存器。

MM0 MM1 MM2 MM3 MM4 MM5 MM6 MM7 每个MMX寄存器为64位宽。在内部,MMX寄存器是x87浮点寄存器的别名。这些底层浮点寄存器为80位长,但MMX只访问其低64位。

这一决定节省了硅片面积,但也造成了一个重要的限制:

别担心……我们很快就会回来详细讨论这些限制。

单个MMX寄存器可以容纳不同的布局:

根据所使用的指令,同一个寄存器可以表示这些布局中的任何一种。

假设我们要将8个像素相加(假设没有编译器优化):

for (int i = 0; i < n; i++) { dst[i] = src1[i] + src2[i]; }

MMX版本看起来像这样:

movq mm0, [src1] ; 加载8个字节 movq mm1, [src2] ; 加载8个字节 paddb mm0, mm1 ; 同时执行8个字节加法 movq [dst], mm0 ; 存储结果 一条指令替代了八条独立的加法指令!

MMX另一个真正出色的特性是能够执行“饱和”(saturating)算术。

当我们将两个值加在一起时,普通整数算术会回绕(wrap around)。

250 + 20 // = 270,回绕后变成14 上面的加法超过了255,通常回绕到14。

整数算术的“回绕”特性处理起来可能很麻烦,尤其是在处理图形时。如果我们要将像素值相加,我们希望最大亮度保持为最大亮度!

MMX针对此类情况提供了饱和功能。指令很简单:

paddusb mm0, mm1 这里的us代表无符号饱和(unsigned saturation),将最大结果值限制为255。

250 + 20 // = 255 同样:

10 - 40 // = 0 这使得亮度调整和混合操作简单得多。

人们很自然会想到MMX的强大功能应用于图形和其他多媒体任务。例如,假设我们要创建一个掩码,其中每个大于另一个的值变为白色。

pcmpgtb mm0, mm1 每个字节变为0xFF或0x00。

这些掩码在实现阈值滤镜、精灵透明或碰撞逻辑时非常有用。

如果你上过我们的课程,你就会知道我们经常使用位移(bitshifting)来乘除2。考虑到在旧CPU中乘法(尤其是除法)指令非常昂贵,向左和向右移位是乘以或除以2的幂的更快方式。

MMX提供了一种同时移位打包值的方法:

psllw mm0, 1 ; 将每个16位值乘以2 psrlw mm0, 1 ; 除以2 同样,所有打包值同时被移位!

需要将打包值乘以2以外的数?没问题。MMX也支持打包乘法。

pmullw mm0, mm1 每对16位整数独立相乘。这在以下场景中变得有用:

还记得MMX的一大特点是它“接管”了x87的浮点寄存器吗?这意味着MMX没有浮点SIMD,因此MMX程序严重依赖定点算术。面向英特尔x86 CPU的真正浮点SIMD直到几年后随奔腾III系列推出的SSE(流式SIMD扩展)才出现。

假设你负责编写一个用于提亮图像的小例程。在这幅图像中,每个像素占一个字节。没有MMX时,我们的代码可能看起来像这样:

int intensity = 20; for (int i = 0; i < num_pixels; i++) { pixels[i] += intensity; if (pixels[i] > 255) { pixels[i] = 255; } } 使用MMX,对像素块进行迭代的代码看起来像这样:

`section .data ; 我们在内存中放入8个值为20(十六进制0x14)的字节 intensity: times 8 db 20 ; 14 14 14 14 14 14 14 14

section .text ; esi = 指向像素的指针 ; ecx = 8像素块的数量

loop: movq mm0, [esi] ; 加载8个像素 movq mm1, [intensity] ; 加载八个20 paddusb mm0, mm1 ; 饱和加法 movq [esi], mm0 ; 存储8个像素

add esi, 8 ; 处理下一个8像素 dec ecx jnz loop

emms ; 退出MMX状态 ` 使用MMX,一次处理八个像素。这正是图形库所钟爱的那种优化。

正如我们刚刚了解的,MMX“借用”了原始x86架构中的浮点寄存器。

这意味着当你不再使用MMX时,必须告诉CPU你已用完。

emms

这会清除MMX状态,使x87浮点指令能够再次正常工作。

如果我们忘记调用EMMS,程序中可能会在稍后出现奇怪的浮点错误。

相信我,每个MMX程序员迟早都会学到这一课。

奇怪的是,实际使用MMX的(游戏)并不像你想象的那么多。除了MMX本身受限之外,你还要记住,MMX发布的时间恰逢GPU和3D硬件加速卡(如S3 ViRGE、ATI 3D Rage和3dfx Voodoo)进入PC市场。因此,到MMX问世时,专用3D硬件已经开始接管那些原本可能受益于CPU端优化的工作负载。

话虽如此,在DOS晚期和Windows 95早期,对于未加速的游戏,CPU通常是瓶颈。

NovaLogic旗下Comanche 3的主要程序员Kyle Freeman经常提到,该游戏在音频和图形方面都大量使用了MMX指令。

需要或受益于软件渲染器中MMX增强功能(或需要MMX增强才能运行)的游戏包括Eraser Turnabout、POD和Extreme Assault。我还读到过一些传闻,称有一个为古墓丽影添加MMX增强(用于光照和纹理)的补丁,但我个人从未见过可靠的来源证实这个版本存在。如果你有相关信息,请告诉我!

尽管Quake和Quake II经常被用作受益于MMX技术的游戏例子,但需要指出的是,Quake著名的软件渲染器其实早于MMX。原版Quake于1996年发布,而英特尔在1997年才推出奔腾MMX。因此,Quake的高性能主要是通过寄存器分配、循环优化、查找表和手写x86等技术实现的;并非依赖MMX。

正如我们稍后将讨论的,实际使用MMX的游戏并不多。但确实使用了MMX的应用程序,会附带可选的MMX代码路径,并在CPU支持新指令时选择该路径。当然,开发人员通常也会维护非MMX版本,因为数百万用户仍在使用较老的奔腾处理器。

软件不能简单地假设MMX可用。常见的方法是使用CPUID指令。

CPUID是一条x86 CPU指令,用于向处理器询问其支持的功能并获取CPU信息。

mov eax, 1 cpuid ; 获取CPU信息 test edx, 1 ; 检查EDX的第23位 EDX寄存器的第23位表示MMX支持。如果该位被置位,程序就可以安全地执行MMX指令。否则,应回退到标量(非MMX)实现。

EDX中的其他位会告诉我们其他受支持的技术。例如:

多年后,Quake II增加了对一项名为3DNow!的技术的支持。3DNow!由AMD设计,概念上与MMX类似;它为基础x86指令集添加了SIMD指令,使用向量寄存器实现浮点SIMD操作。

3DNow!也使用64位MMX寄存器,但增加了面向浮点SIMD的指令。第一款搭载3DNow!技术的处理器是1998年的AMD K6-2。

Id Software于2001年以GPL许可证发布了Quake II 3.19的源代码。但请注意,3DNow!代码并不包含在最初的3.19源码版本中。相反,AMD/id Software分发了一个单独的、经过3DNow!优化的Quake II v3.20构建版本。当然,这些是以预编译二进制形式(没有源代码)分发的。

不。尽管一些热门游戏加入了3DNow!增强,但3DNow!从未比MMX更流行。事实上,MMX要普及得多,因为它有巨大的先发优势。英特尔于1997年1月随奔腾MMX推出了MMX,而AMD的3DNow!直到1998年才随K6-2出现。

更重要的是,英特尔在x86市场占有巨大份额,因此选择MMX意味着可能支持更大比例的PC游戏玩家。归根结底,3DNow!是AMD特有的扩展。

开发人员面临支持英特尔CPU还是AMD CPU的选择。如果你编写了针对3DNow!优化的渲染器,它只对支持该技术的AMD芯片有益。与此同时,MMX得到了英特尔以及随后AMD、Cyrix和其他厂商的支持。

最后,对AMD 3DNow!的致命一击是英特尔在1999年随奔腾III推出了SSE(流式SIMD扩展)。