SIMD 的三个基本缺陷

2021-08-10 00:09:25

根据 Flynn 的分类法,SIMD 是指一种计算机架构,可以用一条指令处理多个数据流(即“单指令流,多个数据流”)。有不同的分类法,并且在被归类为“SIMD”的几个不同的子类别和架构中。然而,在这篇文章中,我指的是当代消费级指令集架构中最常见的 SIMD 类型,大多数人在听到“SIMD”一词时都会想到这种类型:打包 SIMD。打包 SIMD 架构的共同特征是将多个数据元素打包到一个固定宽度的寄存器中。以下是打包的 128 位宽 SIMD 寄存器的可能配置示例:例如,128 位寄存器可以保存 16 个整数字节或 4 个单精度浮点值。自 1990 年代中期以来,这种类型的 SIMD 架构非常流行,一些打包的 SIMD ISA:s 是: 所有这些 ISA:s 的承诺是提高数据处理性能,因为每条指令并行执行多个操作。但是,这个模型存在问题。由于寄存器大小是固定的,如果不添加新指令和寄存器,就无法将 ISA 扩展到新的硬件并行级别。举个例子:MMX(64 位)vs SSE(128 位)vs AVX(256 位)vs AVX-512(512 位)。添加新的寄存器和指令有很多含义。例如,必须更新 ABI,并且必须向操作系统内核、编译器和调试器添加支持。

另一个问题是每个新的 SIMD 生成都需要新的指令操作码和编码。在固定宽度指令集(例如 ARM)中,这可能会禁止任何新的扩展,因为可能没有足够的操作码槽来添加新指令。在可变宽度指令集(例如 x86)中,效果通常是指令变得越来越长(有效地损害了代码密度)。矛盾的是,每一代新的 SIMD 本质上都使前几代变得多余(除了支持二进制向后兼容性),因此浪费了大量指令而没有增加太多价值。最后,任何想要使用新指令集的软件都需要重写(或至少重新编译)。更糟糕的是,软件开发人员通常必须针对多个 SIMD 代,并在他们的程序中添加机制,根据支持的 SIMD 代动态选择最佳代码路径。打包的 SIMD 范例是寄存器宽度和执行单元宽度之间存在 1:1 的映射(例如,NEON 和 SSE 为 128 位)。同时,许多 SIMD 操作是流水线式的,需要几个时钟周期才能完成(例如浮点运算和内存加载指令)。这样做的副作用是一条 SIMD 指令的结果直到指令流中后面的几条指令才准备好使用。因此,必须展开循环以避免停顿并保持流水线忙碌。这可以在具有寄存器重命名和推测性乱序执行的高级(耗电)硬件实现中完成,但对于更简单(通常更节能)的硬件实现,循环必须在软件中展开。许多旨在支持有序和无序处理器的软件开发人员和编译器只是在软件中展开所有 SIMD 循环。然而,循环展开会损害代码密度(即使程序二进制文件更大),这反过来又会损害指令缓存性能(指令缓存中适合的程序段变少,从而降低了缓存命中率)。循环展开也增加了寄存器压力(即必须使用更多的寄存器以将多个循环迭代的状态保持在寄存器中),因此架构必须提供足够的 SIMD 寄存器以避免寄存器溢出。当循环中要处理的数组元素数量不是SIMD寄存器中元素数量的倍数时,需要在软件中实现特殊的循环尾部处理。例如,如果一个数组包含 99 个 32 位元素,并且 SIMD 体系结构为 128 位宽(即一个 SIMD 寄存器包含四个 32 位元素),则可以在主 SIMD 循环中处理 4*24=96 个元素,并且 99 -96=3 个元素需要在主循环后处理。

这在处理尾部的循环之后需要额外的代码。一些架构支持屏蔽加载/存储,这使得使用 SIMD 指令处理尾部成为可能,而更常见的场景是您必须使用标量(非 SIMD)指令来实现尾部(在后一种情况下可能有如果标量和 SIMD 指令具有不同的功能和/或语义,那么问题就来了,但这不是打包 SIMD 本身的问题,只是一些 ISA:s 的设计方式)。通常在循环之前你还需要额外的控制逻辑。例如,如果数组长度小于 SIMD 寄存器宽度,则应跳过主 SIMD 循环。添加的控制逻辑和尾部处理代码损害了代码密度(再次降低了指令缓存效率),并增加了额外的开销(并且通常很难编写代码)。解决上述所有缺陷的打包 SIMD 的一种替代方法是矢量处理器。也许最著名的矢量处理器是 Cray-1(1975 年发布),它启发了新一代指令集架构,包括 ARM SVE 和 RISC-V RVV。其他几个(可能鲜为人知的)项目正在追求类似的矢量模型,包括 Agner Fog 的 ForwardCom 和我自己的 MRISC32。一个有趣的变体是 Libre-SOC(基于 OpenPOWER)及其 Simple-V 扩展,它将向量映射到标量寄存器文件(每个标量寄存器文件都包含大约 128 个寄存器)。 Mitch Alsup 的 My 66000 及其虚拟向量方法 (VVM) 采用了一种完全不同的方法,该方法借助特殊的循环修饰指令在硬件中将标量循环转换为矢量化循环。这样它甚至不必有一个向量寄存器文件。另一个有趣的架构是 Mill,它也支持没有打包 SIMD 的向量。