什么是 SIMD
SIMD(Single Instruction, Multiple Data)译为单指令多数据,是一种并行计算技术,允许单条指令同时对多个数据元素进行操作,从而提高计算效率。
与 SIMD 相对的是 SISD(Single Instruction, Single Data,单指令单数据),即每条指令只处理一个数据元素。
现在的大多数 CPU 都支持 SIMD 指令集,例如 Intel 的 SSE 和 AVX,ARM 的 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 = Vector128<float>.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.Run<SimdBenchmark>(); } }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 基础 API
System.Runtime.Intrinsics 命名空间#
.NET 为我们提供了下面三个命名空间来使用 SIMD 技术:
- System.Runtime.Intrinsics :包含用于创建和传递各种大小和格式的寄存器状态的类型。
- System.Runtime.Intrinsics.X86 :包含特定于 x86/x64 架构的 SIMD 指令集的类型。
- System.Runtime.Intrinsics.Arm :包含特定于 ARM 架构的 SIMD 指令集的类型。
System.Runtime.Intrinsics命名空间中定义了表示不同大小向量的结构体和提供创建及操作这些向量的静态类。
结构体
| 类型 | 描述 |
|---|---|
| Vector64<T> | 表示指定数值类型的 64 位向量,该向量适用于并行算法的低级别优化。 |
| Vector128<T> | 表示指定数值类型的 128 位向量,该向量适用于并行算法的低级别优化。 |
| Vector256<T> | 表示指定数值类型的 256 位向量,该向量适用于并行算法的低级别优化。 |
| Vector512<T> | 表示指定数值类型的 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 SIMD(NEON)指令集的访问。 |
更详细的列表可以参考官方文档:
System.Runtime.Intrinsics.X86 命名空间
System.Runtime.Intrinsics.Arm 命名空间
如何理解向量的大小#
向量的大小(如 64 位、128 位、256 位、512 位)指的是向量寄存器能够容纳的数据总位数。每个向量寄存器可以存储多个数据元素,这些数据元素的类型和数量取决于向量的大小和数据类型的位数。
例如开头用到的Vector128<float>,它表示一个 128 位的向量寄存器,可以存储 4 个 32 位的浮点数(因为 128 / 32 = 4)。
如果是用来存储 64 位的双精度浮点数(double),则Vector128<double>可以存储 2 个双精度浮点数(因为 128 / 64 = 2)。
using System.Runtime.Intrinsics; // 创建一个 128 位的向量,存储 16 个 8 位的 字节 Vector128<byte> 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 位的 浮点数 Vector128<float> vectorFloat = Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f); // 创建一个 256 位的向量,存储 8 个 32 位的 浮点数 Vector256<float> vector256Float = Vector256.Create(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f); // 创建一个 128 位的向量,存储 2 个 64 位的 双精度浮点数 Vector128<double> vectorDouble = Vector128.Create(1.0, 2.0); // 创建一个 256 位的向量,存储 4 个 64 位的 双精度浮点数 Vector256<double> vector256Double = Vector256.Create(1.0, 2.0, 3.0, 4.0);跨平台实现方式#
.NET 的 SIMD 提供了跨平台的实现方式。无论是在 x86/x64 还是 ARM 架构上,.NET 都会根据运行时环境自动选择合适的 SIMD 指令集来执行向量化操作。
VectorXXX 为我们提供了一组静态方法,用于创建和操作向量。例如,Vector128.Add 方法用于对两个 128 位向量执行加法运算。
我们也可以直接使用运算符号来进行向量运算,例如+、-、*、/等。VectorXXX<T>结构体重载了这些运算符,使得向量运算更加直观和简洁。
下面这个例子使用Vector128<float>来进行浮点数的 SIMD 运算:
using System.Runtime.Intrinsics; // 创建两个 128 位的浮点向量 Vector128<float> vectorA = Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f); Vector128<float> 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(Vector128<float>.IsSupported ? "Vector128<float> 受支持" : "Vector128<float> 不受支持");但VectorXXX.IsHardwareAccelerated仅仅表示该宽度的向量操作是否可以使用硬件加速,和VectorXXX<T>.IsSupported并没有关系。
即使Vector512<T>.IsHardwareAccelerated是 false,Vector512<T>.IsSupported也可能是 true,最终会降级为Vector256<T>或Vector128<T>来实现。
Console.WriteLine(Vector512.IsHardwareAccelerated ? "Vector512 支持硬件加速" : "Vector512 不支持硬件加速"); Console.WriteLine(Vector256.IsHardwareAccelerated ? "Vector256 支持硬件加速" : "Vector256 不支持硬件加速"); Console.WriteLine(Vector128.IsHardwareAccelerated ? "Vector128 支持硬件加速" : "Vector128 不支持硬件加速"); Console.WriteLine(Vector128<int>.IsSupported ? "Vector128<int> 支持" : "Vector128<int> 不支持"); Console.WriteLine(Vector256<int>.IsSupported ? "Vector256<int> 支持" : "Vector256<int> 不支持"); Console.WriteLine(Vector512<int>.IsSupported ? "Vector512<int> 支持" : "Vector512<int> 不支持"); Vector512<int> vectorA = Vector512.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); Vector512<int> vectorB = Vector512.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); Vector512<int> result = Vector512.Add(vectorA, vectorB); Console.WriteLine("result: " + result);Vector512 不支持硬件加速 Vector256 不支持硬件加速 Vector128 支持硬件加速 Vector128<int> 支持 Vector256<int> 支持 Vector512<int> 支持 result: <17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17>如果 Vector128.IsSupported 是false,使用 VectorXXX 结构体会抛出 PlatformNotSupportedException 异常。
Console.WriteLine(Vector128<bool>.IsSupported ? "Vector128<bool> 支持" : "Vector128<bool> 不支持"); Vector128<bool> v = new Vector128<bool>(); v = v.WithElement(0, true);Vector128<bool> 不支持 Unhandled exception. System.NotSupportedException: Specified type is not supported at System.Runtime.Intrinsics.Vector128`1.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 位的浮点向量 Vector128<float> vectorA = Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f); Vector128<float> 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 位的浮点向量 Vector128<float> vectorA = Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f); Vector128<float> vectorB = Vector128.Create(5.0f, 6.0f, 7.0f, 8.0f); // 使用 AdvSimd 指令集执行加法运算 var result = AdvSimd.Add(vectorA, vectorB); // 输出结果 Console.WriteLine($"Result: {result}");