使用 SIMD 加速的数字类型

SIMD(单指令多数据)为使用单指令并行对多条数据执行操作提供了硬件支持。 在 .NET 中,System.Numerics 命名空间下有一组 SIMD 加速类型。 SIMD 操作可以在硬件级别并行化。 这可以增加向量化计算的吞吐量,这在数学、科学和图形应用中很常见。

.NET SIMD 加速类型

.NET SIMD 加速类型包括以下类型:

  • Vector2Vector3Vector4 类型,用于表示具有 2、3 和 4 Single 值的向量。

  • 两个矩阵类型:Matrix3x2(表示 3x2 矩阵)和 Matrix4x4(表示 Single 值的 4x4 矩阵)。

  • Plane 类型,表示使用 Single 值的三维空间中的一个平面。

  • Quaternion 类型,表示一个用于使用 Single 值来对三维物理旋转进行编码的向量。

  • Vector<T> 类型,表示指定数字类型的向量,并提供受益于 SIMD 支持的一组广泛的运算符。 Vector<T> 实例的计数在应用程序的生命周期内是固定的,但其值 Vector<T>.Count 取决于运行代码的机器的 CPU。

    注意

    Vector<T> 类型未包含在 .NET Framework 中。 必须安装 System.Numerics.Vectors NuGet 包才能访问此类型。

SIMD 加速的类型以这样一种方式实现:即它们可以与非 SIMD 加速的硬件或 JIT 编译器一起使用。 要利用 SIMD 指令,你的 64 位应用必须由使用 RyuJIT 编译器的运行时运行。 .NET Core 和 .NET Framework 4.6 及更高版本中包含 RyuJIT 编译器。 仅当面向 64 位处理器时才提供 SIMD 支持。

如何使用 SIMD?

在执行自定义 SIMD 算法之前,可以使用 Vector.IsHardwareAccelerated 来检查主机是否支持 SIMD(返回一个 Boolean)。 这并不能保证为特定类型启用 SIMD 加速,但表明某些类型支持它。

简单矢量

.NET 中最原始的 SIMD 加速类型是 Vector2Vector3Vector4 类型,分别表示具有 2、3 和 4 个 Single 值的矢量。 下面的示例使用过 Vector2 来添加两个矢量。

var v1 = new Vector2(0.1f, 0.2f);
var v2 = new Vector2(1.1f, 2.2f);
var vResult = v1 + v2;

也可以使用 .NET 矢量来计算矢量的其他数学属性,例如 Dot productTransformClamp 等。

var v1 = new Vector2(0.1f, 0.2f);
var v2 = new Vector2(1.1f, 2.2f);
var vResult1 = Vector2.Dot(v1, v2);
var vResult2 = Vector2.Distance(v1, v2);
var vResult3 = Vector2.Clamp(v1, Vector2.Zero, Vector2.One);

Matrix

Matrix3x2 表示 3x2 矩阵,Matrix4x4 表示 4x4 矩阵。 可用于与矩阵相关的计算。 下面的示例演示了使用 SIMD 将矩阵与其对应的转置矩阵相乘。

var m1 = new Matrix4x4(
            1.1f, 1.2f, 1.3f, 1.4f,
            2.1f, 2.2f, 3.3f, 4.4f,
            3.1f, 3.2f, 3.3f, 3.4f,
            4.1f, 4.2f, 4.3f, 4.4f);

var m2 = Matrix4x4.Transpose(m1);
var mResult = Matrix4x4.Multiply(m1, m2);

Vector<T>

Vector<T> 提供了使用更长矢量的能力。 Vector<T> 实例的计数是固定的,但其值 Vector<T>.Count 取决于运行代码的计算机的 CPU。

以下示例演示如何使用 Vector<T> 计算两个数组的元素总和。

double[] Sum(double[] left, double[] right)
{
    if (left is null)
    {
        throw new ArgumentNullException(nameof(left));
    }

    if (right is null)
    {
        throw new ArgumentNullException(nameof(right));
    }

    if (left.Length != right.Length)
    {
        throw new ArgumentException($"{nameof(left)} and {nameof(right)} are not the same length");
    }

    int length = left.Length;
    double[] result = new double[length];

    // Get the number of elements that can't be processed in the vector
    // NOTE: Vector<T>.Count is a JIT time constant and will get optimized accordingly
    int remaining = length % Vector<double>.Count;

    for (int i = 0; i < length - remaining; i += Vector<double>.Count)
    {
        var v1 = new Vector<double>(left, i);
        var v2 = new Vector<double>(right, i);
        (v1 + v2).CopyTo(result, i);
    }

    for (int i = length - remaining; i < length; i++)
    {
        result[i] = left[i] + right[i];
    }

    return result;
}

注解

SIMD 更有可能消除一个瓶颈但又暴露下一个瓶颈,例如内存吞吐量。 一般来说,使用 SIMD 的性能优势取决于具体的场景,在某些情况下,它的性能甚至可能比更简单的非 SIMD 等效代码更差。