
SIMDSingle Instruction, Multiple Data译为单指令多数据是一种并行计算技术允许单条指令同时对多个数据元素进行操作从而提高计算效率。与 SIMD 相对的是 SISDSingle Instruction, Single Data单指令单数据即每条指令只处理一个数据元素。现在的大多数 CPU 都支持 SIMD 指令集例如 Intel 的 SSE 和 AVXARM 的 NEON 等。如果我们要对两组数组进行加法运算传统方法SISD是逐个元素相加而使用 SIMD 技术可以一次性将多个元素加载到向量寄存器中并执行单一的加法指令从而显著提高计算效率。下面我们通过一个简单的示例对比传统的数组加法和使用 SIMD 优化后的数组加法在性能上的差异。例子中会对两个浮点数组进行加法运算把结果存储在第三个数组中。using System.Runtime.Intrinsics; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; [MemoryDiagnoser] public class SimdBenchmark { private float[] _arrA; private float[] _arrB; private float[] _resultArray; private readonly int _dataSize 1_000_000; [GlobalSetup] public void Setup() { var random new Random(); _arrA new float[_dataSize]; _arrB new float[_dataSize]; _resultArray new float[_dataSize]; for (int i 0; i _dataSize; i) { _arrA[i] (float)random.NextDouble() * 10f; _arrB[i] (float)random.NextDouble() * 10f; } } [Benchmark] public void NormalAdd() { for (int i 0; i _dataSize; i) { _resultArray[i] _arrA[i] _arrB[i]; } } [Benchmark] public void SimdAdd() { // 每次处理 4 个元素 int simdLength Vector128float.Count; // 4 int i 0; // 处理可被 SIMD 整除的部分 for (; i _dataSize - simdLength; i simdLength) { // 表示从数组的第 i 个位置开始加载数据到向量中, 每次加载 4 个 float var va Vector128.LoadUnsafe(ref _arrA[i]); var vb Vector128.LoadUnsafe(ref _arrB[i]); (va vb).CopyTo(_resultArray, i); } // 处理尾部不足 4 个的元素 for (; i _dataSize; i) { _resultArray[i] _arrA[i] _arrB[i]; } } } public class Program { public static void Main(string[] args) { BenchmarkRunner.RunSimdBenchmark(); } }BenchmarkDotNet v0.15.6, macOS Sequoia 15.7.2 (24G325) [Darwin 24.6.0] Apple M2 Max, 1 CPU, 12 logical and 12 physical cores .NET SDK 10.0.100 [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a | Method | Mean | Error | StdDev | Allocated | |---------- |---------:|--------:|--------:|----------:| | NormalAdd | 901.8 us | 4.36 us | 3.64 us | - | | SimdAdd | 300.2 us | 2.56 us | 2.27 us | - |笔者在 MacBook Pro M2 Max 上测试使用 SIMD 优化后的数组加法运算相比传统方法性能提升了约 3 倍。此例子也可以在 Windows 和 Linux 上运行有兴趣的读者可以自行测试不同平台的性能差异。SIMD 基础 APISystem.Runtime.Intrinsics 命名空间#.NET 为我们提供了下面三个命名空间来使用 SIMD 技术System.Runtime.Intrinsics 包含用于创建和传递各种大小和格式的寄存器状态的类型。System.Runtime.Intrinsics.X86 包含特定于 x86/x64 架构的 SIMD 指令集的类型。System.Runtime.Intrinsics.Arm 包含特定于 ARM 架构的 SIMD 指令集的类型。System.Runtime.Intrinsics命名空间中定义了表示不同大小向量的结构体和提供创建及操作这些向量的静态类。结构体类型描述Vector64T表示指定数值类型的 64 位向量该向量适用于并行算法的低级别优化。Vector128T表示指定数值类型的 128 位向量该向量适用于并行算法的低级别优化。Vector256T表示指定数值类型的 256 位向量该向量适用于并行算法的低级别优化。Vector512T表示指定数值类型的 512 位向量该向量适用于并行算法的低级别优化。静态类类型描述Vector64提供静态方法的集合用于在 64 位向量上创建、操作和以其他方式操作。Vector128提供静态方法集合用于在 128 位向量上创建、操作和以其他方式操作。Vector256提供静态方法集合用于在 256 位向量上创建、操作和以其他方式操作。Vector512提供静态方法的集合用于在 512 位向量上创建、操作和以其他方式操作。System.Runtime.Intrinsics.X86和System.Runtime.Intrinsics.Arm命名空间中定义了特定于各自架构的 SIMD 指令集的类这些类提供了访问底层硬件 SIMD 指令的能力。常见的指令集类例如类型描述Sse提供对 x86/x64 SSE 指令集的访问。Sse2提供对 x86/x64 SSE2 指令集的访问。Avx提供对 x86/x64 AVX 指令集的访问。Avx2提供对 x86/x64 AVX2 指令集的访问。AdvSimd提供对 ARM Advanced SIMDNEON指令集的访问。更详细的列表可以参考官方文档System.Runtime.Intrinsics.X86 命名空间System.Runtime.Intrinsics.Arm 命名空间如何理解向量的大小#向量的大小如 64 位、128 位、256 位、512 位指的是向量寄存器能够容纳的数据总位数。每个向量寄存器可以存储多个数据元素这些数据元素的类型和数量取决于向量的大小和数据类型的位数。例如开头用到的Vector128float它表示一个 128 位的向量寄存器可以存储 4 个 32 位的浮点数因为 128 / 32 4。如果是用来存储 64 位的双精度浮点数double则Vector128double可以存储 2 个双精度浮点数因为 128 / 64 2。using System.Runtime.Intrinsics; // 创建一个 128 位的向量存储 16 个 8 位的 字节 Vector128byte vectorByte Vector128.Create((byte)1, (byte)2, (byte)3, (byte)4, (byte)5, (byte)6, (byte)7, (byte)8, (byte)9, (byte)10, (byte)11, (byte)12, (byte)13, (byte)14, (byte)15, (byte)16); // 创建一个 128 位的向量存储 4 个 32 位的 浮点数 Vector128float vectorFloat Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f); // 创建一个 256 位的向量存储 8 个 32 位的 浮点数 Vector256float vector256Float Vector256.Create(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f); // 创建一个 128 位的向量存储 2 个 64 位的 双精度浮点数 Vector128double vectorDouble Vector128.Create(1.0, 2.0); // 创建一个 256 位的向量存储 4 个 64 位的 双精度浮点数 Vector256double vector256Double Vector256.Create(1.0, 2.0, 3.0, 4.0);跨平台实现方式#.NET 的 SIMD 提供了跨平台的实现方式。无论是在 x86/x64 还是 ARM 架构上.NET 都会根据运行时环境自动选择合适的 SIMD 指令集来执行向量化操作。VectorXXX 为我们提供了一组静态方法用于创建和操作向量。例如Vector128.Add 方法用于对两个 128 位向量执行加法运算。我们也可以直接使用运算符号来进行向量运算例如、-、*、/等。VectorXXXT结构体重载了这些运算符使得向量运算更加直观和简洁。下面这个例子使用Vector128float来进行浮点数的 SIMD 运算using System.Runtime.Intrinsics; // 创建两个 128 位的浮点向量 Vector128float vectorA Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f); Vector128float vectorB Vector128.Create(5.0f, 6.0f, 7.0f, 8.0f); // 执行加法运算 // 等效于 vectorA vectorB var result Vector128.Add(vectorA, vectorB); // 输出结果 Console.WriteLine($Result: {result});Result: 6, 8, 10, 12可以使用VectorXXX.IsHardwareAccelerated 检查某个宽度是可以硬件加速VectorXXX.IsSupported 检查特定宽度类型组合是否可用。// 检查宽度是否有硬件加速128位 Console.WriteLine(Vector128.IsHardwareAccelerated ? 128 位向量操作可硬件加速 : 128 位向量操作不支持硬件加速); // 检查某种具体类型的向量是否被支持 Console.WriteLine(Vector128float.IsSupported ? Vector128float 受支持 : Vector128float 不受支持);但VectorXXX.IsHardwareAccelerated仅仅表示该宽度的向量操作是否可以使用硬件加速和VectorXXXT.IsSupported并没有关系。即使Vector512T.IsHardwareAccelerated是 falseVector512T.IsSupported也可能是 true最终会降级为Vector256T或Vector128T来实现。Console.WriteLine(Vector512.IsHardwareAccelerated ? Vector512 支持硬件加速 : Vector512 不支持硬件加速); Console.WriteLine(Vector256.IsHardwareAccelerated ? Vector256 支持硬件加速 : Vector256 不支持硬件加速); Console.WriteLine(Vector128.IsHardwareAccelerated ? Vector128 支持硬件加速 : Vector128 不支持硬件加速); Console.WriteLine(Vector128int.IsSupported ? Vector128int 支持 : Vector128int 不支持); Console.WriteLine(Vector256int.IsSupported ? Vector256int 支持 : Vector256int 不支持); Console.WriteLine(Vector512int.IsSupported ? Vector512int 支持 : Vector512int 不支持); Vector512int vectorA Vector512.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); Vector512int vectorB Vector512.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); Vector512int result Vector512.Add(vectorA, vectorB); Console.WriteLine(result: result);Vector512 不支持硬件加速 Vector256 不支持硬件加速 Vector128 支持硬件加速 Vector128int 支持 Vector256int 支持 Vector512int 支持 result: 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17如果 Vector128.IsSupported 是false使用 VectorXXX 结构体会抛出 PlatformNotSupportedException 异常。Console.WriteLine(Vector128bool.IsSupported ? Vector128bool 支持 : Vector128bool 不支持); Vector128bool v new Vector128bool(); v v.WithElement(0, true);Vector128bool 不支持 Unhandled exception. System.NotSupportedException: Specified type is not supported at System.Runtime.Intrinsics.Vector1281.get_Count()SIMD 指令集的使用#在使用 SIMD 指令集之前通常需要检查当前平台是否支持特定的指令集。可以通过调用指令集类的 IsSupported 属性来进行检查。例如using System.Runtime.Intrinsics.X86; Console.WriteLine(Sse.IsSupported ? SSE 指令集受支持 : SSE 指令集不受支持);一旦确认指令集受支持就可以使用该指令集类提供的静态方法来执行 SIMD 操作。例如使用 Sse 类的 Add 方法来对两个 128 位向量执行加法运算using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; if (Sse.IsSupported) { // 创建两个 128 位的浮点向量 Vector128float vectorA Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f); Vector128float vectorB Vector128.Create(5.0f, 6.0f, 7.0f, 8.0f); // 使用 SSE 指令集执行加法运算 var result Sse.Add(vectorA, vectorB); // 输出结果 Console.WriteLine($Result: {result}); } else { Console.WriteLine(SSE 指令集不受支持); }Result: 6, 8, 10, 12如果是在 ARM 架构上可以使用 AdvSimd 类来执行类似的操作using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; if (AdvSimd.IsSupported) { // 创建两个 128 位的浮点向量 Vector128float vectorA Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f); Vector128float vectorB Vector128.Create(5.0f, 6.0f, 7.0f, 8.0f); // 使用 AdvSimd 指令集执行加法运算 var result AdvSimd.Add(vectorA, vectorB); // 输出结果 Console.WriteLine($Result: {result});