Compartir por


Uso de tipos numéricos acelerados por SIMD

SIMD (instrucción única, varios datos) proporciona compatibilidad de hardware para realizar una operación en varios fragmentos de datos, en paralelo, mediante una sola instrucción. En .NET, hay un conjunto de tipos acelerados por SIMD en el System.Numerics espacio de nombres . Las operaciones SIMD se pueden paralelizar en el nivel de hardware. Esto aumenta el rendimiento de los cálculos vectorizados, que son comunes en las aplicaciones matemáticas, científicas y gráficas.

Tipos de .NET acelerados por SIMD

Los tipos acelerados por SIMD de .NET incluyen los siguientes tipos:

  • Los Vector2tipos , Vector3y Vector4 , que representan vectores con 2, 3 y 4 Single valores.

  • Dos tipos de matriz, , Matrix3x2que representa una matriz de 3x2 y Matrix4x4, que representa una matriz de 4x4 de Single valores.

  • El tipo Plane, que representa un plano en el espacio tridimensional utilizando valores de Single.

  • Tipo Quaternion , que representa un vector que se usa para codificar rotaciones físicas tridimensionales mediante Single valores.

  • El Vector<T> tipo, que representa un vector de un tipo numérico especificado y proporciona un amplio conjunto de operadores que se benefician de la compatibilidad con SIMD. El recuento de una Vector<T> instancia se fija durante la vigencia de una aplicación, pero su valor Vector<T>.Count depende de la CPU de la máquina que ejecuta el código.

    Nota:

    El Vector<T> tipo no se incluye en .NET Framework. Debe instalar el paquete NuGet System.Numerics.Vectors para obtener acceso a este tipo.

Los tipos acelerados por SIMD se implementan de tal manera que se pueden usar con compiladores JIT o hardware no acelerado por SIMD. Para aprovechar las instrucciones SIMD, las aplicaciones de 64 bits deben ser ejecutadas por el entorno de ejecución que utiliza el compilador RyuJIT. Un compilador de RyuJIT se incluye en .NET Core y en .NET Framework 4.6 y versiones posteriores. La compatibilidad con SIMD solo se proporciona cuando el destino es procesadores de 64 bits.

¿Cómo usar SIMD?

Antes de ejecutar algoritmos SIMD personalizados, es posible comprobar si la máquina host admite SIMD mediante Vector.IsHardwareAccelerated, que devuelve un Boolean. Esto no garantiza que la aceleración de SIMD esté habilitada para un tipo específico, pero es un indicador de que es compatible con algunos tipos.

Vectores simples

Los tipos acelerados por SIMD más primitivos de .NET son Vector2, Vector3y , y Vector4 , que representan vectores con 2, 3 y 4 Single valores. En el ejemplo siguiente se usa Vector2 para agregar dos vectores.

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

También es posible usar vectores de .NET para calcular otras propiedades matemáticas de vectores como Dot product, Transform, etc Clamp .

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);

Matriz

Matrix3x2, que representa una matriz de 3x2 y Matrix4x4, que representa una matriz 4x4. Se puede usar para los cálculos relacionados con la matriz. En el ejemplo siguiente se muestra la multiplicación de una matriz a su matriz transpuesta correspondiente mediante 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> proporciona la capacidad de usar vectores más largos. El recuento de una Vector<T> instancia es fijo, pero su valor Vector<T>.Count depende de la CPU de la máquina que ejecuta el código.

En el ejemplo siguiente se muestra cómo calcular la suma de elementos de dos matrices mediante 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;
}

Observaciones

Es más probable que SIMD quite un cuello de botella y exponga el siguiente, por ejemplo, el rendimiento de memoria. En general, la ventaja de rendimiento del uso de SIMD varía en función del escenario específico y, en algunos casos, incluso puede funcionar peor que el código equivalente no SIMD más sencillo.