90年代SIMD技术:编程 Intel Pentium MMX

Hacker News Top 工具

摘要

本文概述了Intel的Pentium MMX SIMD指令的历史,这些指令于1990年代引入,并解释了它们在将并行处理技术引入主流桌面CPU以支持多媒体应用方面的关键作用。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/16 21:45

# 90年代的SIMD:编程英特尔奔腾MMX 来源:https://pikuma.com/blog/programming-intel-pentium-mmx-simd 如果您在90年代末为PC编程,很可能还记得“Intel Inside Pentium with MMX”的贴纸。MMX固然是一大营销热词,但它也是英特尔将SIMD(单指令多数据)指令引入主流桌面CPU的重要尝试。完成我们软件渲染(https://pikuma.com/courses/learn-3d-computer-graphics-programming)课程的学生,总会开始提问并探索SIMD(单指令多数据)这一主题。如今,我们拥有AVX-512(https://en.wikipedia.org/wiki/AVX-512)、NEON(https://www.arm.com/technologies/neon)、SVE(https://support.arm.com/architectures/scalable-vector-extensions),以及拥有数千核心的GPU。这些现代CPU和扩展技术是多年探索、学习和市场压力的产物,它们也承载着大量历史与向后兼容性的复杂因素。一如既往,我喜欢通过回顾技术诞生之初的情况,来分析和讨论这些不同的技术。回到90年代末,许多程序员正在学习如何通过手写汇编,从奔腾MMX处理器中榨取额外的几帧性能。 **奔腾MMX** 英特尔奔腾MMX技术 让我们穿越回1997年,看看当时编程MMX是什么样子! ### 什么是MMX? MMX(多媒体扩展)于1997年随奔腾MMX处理器一同推出,旨在提升多媒体体验,涵盖图形、数字信号处理、音频和调制解调器软件。它是一组内置于奔腾芯片中的57条*额外*指令,用以增强性能。CPU必须切换至MMX模式,这会将x86架构中八个80位浮点寄存器的前64位转换为MMX寄存器。 **MMX寄存器** 八个MMX寄存器(长64位) 创造和使用MMX的核心思想很简单:CPU不是一次处理一个整数,而是同时处理打包在一个64位寄存器中的多个整数。对于多媒体软件(图像处理、音频混合、视频回放甚至游戏),这能带来显著的加速。例如,不是逐个相加八个字节: ``` 10 + 20 30 + 40 50 + 60 70 + 80 ... ``` MMX让我们可以用*一条指令*执行**全部**八个加法。这就是并行!也是SIMD背后的核心理念。 ### 什么是SIMD? “单指令多数据”是一种并行处理技术。它描述了拥有多个处理单元的计算机,能**同时**对*多个数据点*执行*相同操作*。 **SIMD 单指令多数据** SIMD可以是内生的(硬件设计的一部分),并可通过指令集架构(ISA)直接访问。以MMX为例,英特尔**扩展**了其x86指令集,以包含新的SIMD指令。 ### MMX是第一个SIMD吗? 不,MMX**绝对不是**第一个SIMD,尽管它在将SIMD普及到x86 PC上至关重要。我们必须记住,计算机技术的许多进步和创新都早于个人电脑时代。SIMD也不例外,它比英特尔的MMX早了几十年。使用单个指令操作多个数据元素的通用理念,远早于奔腾处理器。一个著名的早期例子是60年代的ILLIAC IV(https://en.wikipedia.org/wiki/ILLIAC_IV)超级计算机项目。ILLIAC IV是第一台大规模并行计算机。该系统最初设计有256个64位浮点单元(FPU)和四个中央处理单元(CPU),每秒能处理十亿次操作。 **ILLIAC IV计算机** ILLIAC IV 照片由 Sascha Pohlflepp 拍摄,采用 CC-BY-2.0 协议 总之,ILLIAC IV使用了大量在单个指令流下运行的处理单元,使其成为现代SIMD/向量架构的重要祖先。 **ILLIAC IV计算机** ILLIAC IV 阵列概览 此外,在60年代到80年代间,还有来自CDC(https://en.wikipedia.org/wiki/Control_Data_Corporation)、Cray(https://en.wikipedia.org/wiki/Cray)等公司的阵列处理机。 ##### SIMD与阵列处理器 需要指出的是,向量处理(Vector Processing)与SIMD并不完全相同。区别主要在于多个数据元素如何呈现给处理器以及硬件如何执行它们。SIMD操作本质上与寄存器的宽度相关。 **SIMD固定大小元素** 1976年的Cray-1的特点是它是一台*向量处理器*。它有八个64元素的向量寄存器,每个寄存器存放64位值。 **Cray-1超级计算机** 与我们看到的SIMD类似,Cray-1不需要64条单独的ADD指令。那么,既然它显然类似SIMD,为什么我们不直接称之为SIMD呢?因为架构上的区别。SIMD在*固定宽度*的寄存器上操作,而Cray-1中的向量寄存器更像是一个*序列容器*。 **向量处理器处理整个阵列** 向量处理器通常的特点是存在一条`SET VECTOR SIZE`指令。这个向量功能单元可以在多个周期内处理元素。Cray-1有一个**VL(向量长度)**寄存器,以及可以设置该长度的指令。向量功能单元随后在多个周期内处理该数量的元素。数组中所有剩余的额外元素则不被使用/循环。 **VL寄存器** Cray-1 Cray-1的VL寄存器是7位的,尽管该架构只使用到64(2^6)的值,因为Cray-1向量寄存器最多包含64个元素。另外值得一提的一个小细节是,Cray-1将VL=0解释为64,即处理向量寄存器中的全部64个元素。 ### MMX的真正意义 如您所见,SIMD并非英特尔发明的。其真正意义在于将一套相对*易用*的SIMD指令集放入了主流x86 PC中。这是我们大多数人(个人电脑爱好者)第一次能够亲手实践这类技术,这也是为什么MMX在复古计算讨论中备受关注。它并非SIMD的发明,但它是SIMD从专业/高性能计算跨入普通桌面软件开发的重要时刻之一。 ### MMX寄存器 如前所述,一个聪明的设计决策是英特尔没有添加全新的寄存器文件。相反,MMX重用了浮点堆栈寄存器。 ``` MM0 MM1 MM2 MM3 MM4 MM5 MM6 MM7 ``` 每个MMX寄存器宽度为64位。内部而言,MMX寄存器是x87浮点寄存器的**别名**。这些底层的浮点寄存器长80位,但MMX只访问其低64位。 **MMX寄存器** 八个MMX寄存器(长64位) 这一决策节省了硅片面积,但也造成了一个重要限制:SIMD和浮点操作是**互斥的**。由于MMX本质上是“接管”了x87浮点寄存器,因此MMX多媒体操作和浮点操作无法共存。这也创造了一条重要规则:不要混合MMX和x87浮点代码,除非先进行清理。别担心……我们很快会回来详细讨论这些限制。 ### 打包整数数据 单个MMX寄存器可以容纳不同的布局: **奔腾寄存器布局** 根据所使用的指令,同一个寄存器可以代表这些布局中的任何一种。 ### 您的第一个MMX程序 假设我们要将8个像素加在一起(假设没有编译器优化): ``` for (int i = 0; i < 8; i++) { dest[i] = src1[i] + src2[i]; } ``` MMX版本可能看起来像这样: ``` movq mm0, [src1] ; 加载八个字节 movq mm1, [src2] ; 加载八个字节 paddb mm0, mm1 ; 同时执行八个字节加法 movq [dst], mm0 ; 存储结果 ``` 一条指令替代了八条独立的加法! ##### MMX中的饱和算术 MMX的另一个非常棒的功能是能够执行“饱和”算术。当我们把两个值加在一起时,普通的整数算术会**回绕**。 ``` 250 + 20 // = 270,回绕为 14 ``` 上述加法超过255,通常会回绕为14。整数算术的这种“回绕”特性在处理时可能很麻烦,尤其是在处理图形时。如果我们将像素值相加,我们希望最大亮度保持最大亮度!MMX为这种情况提供了饱和处理。指令很简单: ``` paddusb mm0, mm1 ``` 其中的`us`代表*无符号饱和*,它将最大结果值钳位在255。 ``` 250 + 20 // = 255 ``` 同样: ``` 10 - 40 // = 0 ``` 这使得亮度调整和混合操作变得简单得多。 ##### 使用MMX比较像素 自然地,我们会想到将MMX的能力应用于图形和其他多媒体任务。例如,假设我们想创建一个掩码,其中每个大于另一个值的值都变为白色。MMX提供了比较指令: ``` pcmpgtb mm0, mm1 ``` 每个字节变为`0xFF`或`0x00`。这些掩码在实现阈值过滤器、精灵图透明度或碰撞逻辑时非常有用。 ##### 使用MMX移位打包值 如果您上过我们的课程,就知道我们经常使用*位移位*来将值乘以或除以2。鉴于在旧CPU中乘法和(尤其是)除法指令非常昂贵,左移和右移位是将值乘以或除以2的幂的更快方法。MMX提供了一种同时移位打包值的方法: ``` psllw mm0, 1 ; 将每个16位值乘以2 psrlw mm0, 1 ; 除以2 ``` 同样,所有打包的值同时被移位! ##### 使用MMX进行乘法 需要将打包的值乘以2以外的数?没问题。MMX也支持打包乘法。 ``` pmullw mm0, mm1 ``` 每对16位整数独立相乘。这对于以下情况变得有用: - 音频音量缩放 - 图像卷积 - 颜色变换 - 定点数学 还记得MMX的一大方面是它“接管”了x87的浮点寄存器吗?这意味着MMX没有浮点SIMD,因此MMX程序严重依赖定点算术。英特尔x86 CPU真正的浮点SIMD直到几年后随Pentium III(https://en.wikipedia.org/wiki/Pentium_III)系列推出的SSE(https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions)(流式SIMD扩展)才出现。 ### 一个实际例子 假设您负责编写一个小例程来提亮一张图像。在这张图像中,每个像素长一个字节。没有MMX,我们的代码可能看起来像这样: ``` int intensity = 20; for (int i = 0; i < WIDTH * HEIGHT; 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,一次处理八个像素。这正是图形库所喜欢的那种优化。 ### EMMS指令 正如我们刚学的,MMX“借用”了原始x86架构的浮点寄存器。这意味着当您使用完MMX后,必须告诉CPU您已完成。 ``` emms ``` 这会清除MMX状态,并允许x87浮点指令再次正常工作。如果我们忘记调用EMMS,奇怪的浮点错误可能会在程序稍后出现。相信我,每个MMX程序员迟早都会学到这一课。 ### 游戏使用MMX吗? 奇怪的是,并不像您想象的那么多。除了MMX的局限性,您必须记住MMX发布的时间点,恰好与GPU和3D硬件加速卡(如S3 ViRGE(https://en.wikipedia.org/wiki/S3_ViRGE)、ATI 3D Rage(https://en.wikipedia.org/wiki/ATI_Rage)和3dfx Voodoo(https://en.wikipedia.org/wiki/3dfx))进入PC市场的时间重合。因此,当MMX出现时,专用的3D硬件已经开始接管那些本可能受益于CPU端优化的工作负载。 话虽如此,在DOS晚期和Windows 95早期,CPU往往是未加速游戏的性能瓶颈。MMX被用于诸如以下的例程中: - 软件纹理映射 - 精灵图合成 - Alpha混合 - 图像缩放 - MP3解码 - 调制解调器软件 - MPEG视频回放 - 音频混合 Kyle Freeman,Novalogic的Comanche 3(https://en.wikipedia.org/wiki/Comanche_3)的主要程序员,经常提到该游戏如何大量使用MMX指令处理音频和图形。 **Kyle Freeman** Comanche 3 MMX Kyle Freeman关于Comanche 3使用MMX的推文(https://x.com/TheKyleFreeman/status/1623808006218715136?s=20) 在软件渲染器中需要或受益于MMX增强的游戏例子包括Eraser Turnabout(https://en.wikipedia.org/wiki/Eraser:_Turnabout)、POD(https://en.wikipedia.org/wiki/POD_(video_game))和Extreme Assault(https://en.wikipedia.org/wiki/Extreme_Assault)。我曾读过传言称,有一个《古墓丽影》(https://en.wikipedia.org/wiki/Tomb_Raider_(1996_video_game))补丁为游戏的光照和纹理添加了MMX增强,但我个人从未看到任何可靠来源证实该版本存在。如果您知道,请告诉我(https://x.com/pikuma)! 尽管《雷神之锤》(https://en.wikipedia.org/wiki/Quake_(video_game))和《雷神之锤II》(https://en.wikipedia.org/wiki/Quake_II)经常被用作受益于MMX技术的游戏例子,但重要的是要指出,《雷神之锤》著名的软件渲染器早于MMX。原版《雷神之锤》于1996年发布,而英特尔于1997年推出奔腾MMX。因此,《雷神之锤》的快速性能主要是通过寄存器分配、循环优化、查找表和手写x86等技术实现的;**不是**依靠MMX。 正如我们即将讨论的,实际上并没有多少游戏使用MMX。但那些使用的游戏,在发布时会包含可选的MMX代码路径,如果CPU支持新指令,则会选择该路径。当然,开发者通常也维护一个非MMX版本,因为数百万用户仍然拥有旧款奔腾处理器。 ### 检测MMX支持 软件不能简单地假设MMX可用。常见的方法是使用`CPUID`指令。CPUID是一条x86 CPU指令,用于查询处理器支持的功能并获取CPU信息。 ``` mov eax, 1 cpuid ; 获取CPU信息 test edx, 1 << 23 ; EDX的第23位表示MMX支持 jz NoMMX ; 如果为零,跳转到处理MMX缺失的例程 ``` EDX寄存器的第23位表示MMX支持。如果该位被置位,程序可以安全地执行MMX指令。否则,它应该回退到标量(非MMX)实现。EDX中的其他位将告诉我们关于不同支持技术的信息。例如: - 第23位:MMX支持 - 第25位:SSE支持 - 第26位:SSE2支持 ### AMD的3DNow!呢? 几年后,《雷神之锤II》增加了一项名为3DNow!(https://en.wikipedia.org/wiki/3DNow!)的技术支持。3DNow!由AMD设计,其概念类似于MMX;它在基本x86指令集上添加了SIMD指令,支持浮点SIMD操作。

相似文章

每个人都应该了解SIMD

Hacker News Top

Mitchell Hashimoto的一篇博文,认为SIMD(单指令多数据)比通常认为的要简单,并通过Zig示例演示了在循环中使用SIMD的常见模式。

让编写跨平台 SIMD 代码变得愉快

Lobsters Hottest

作者详细介绍了 bx 库跨平台 SIMD 抽象的第三次迭代,倡导无类型方法和 SSA 风格编码,以简化不同 CPU 架构上的底层性能优化。

https://www.youtube.com/watch?v=aE0onltJlOo

YouTube AI Channels

该讲座介绍了GPU架构作为SIMD(向量/数组)处理器的灵活演化,讨论了数据并行性、存储体分组、体冲突、串行瓶颈以及SIMD指令历史(如MMX),强调GPU如何利用数据并行性并应对串行瓶颈。

矩阵转置的实现要点

Hacker News Top

一篇深入的技术博客文章,解释如何使用现代x86_64 CPU上的SIMD指令高效地转置矩阵,重点介绍类似_mm256_shuffle_epi8的AVX2内联函数。