Compartilhar via


Usar tipos numéricos acelerados por SIMD

O SIMD (instrução única, vários dados) fornece suporte a hardware para executar uma operação em vários dados, em paralelo, usando uma só instrução. No .NET, há um conjunto de tipos acelerados por SIMD no namespace System.Numerics. Operações SIMD podem ser paralelizadas no nível de hardware. Isso aumenta a taxa de transferência dos cálculos vetorizadas, que são comuns em aplicativos matemáticos, científicos e gráficos.

Tipos acelerados por SIMD do .NET

Os tipos acelerados por SIMD do .NET incluem os seguintes tipos:

  • Os tipos Vector2, Vector3 e Vector4, que representam vetores com 2, 3 e 4 valores de Single.

  • Dois tipos de matriz, Matrix3x2, que representa uma matriz 3x2, e Matrix4x4, que representa uma matriz 4x4 de valores Single.

  • O tipo Plane representa um plano no espaço tridimensional usando valores Single.

  • O tipo Quaternion, que representa um vetor usado para codificar rotações físicas tridimensionais usando valores Single.

  • O tipo Vector<T>, que representa um vetor de um tipo numérico especificado e fornece um amplo conjunto de operadores que se beneficiam de suporte a SIMD. A contagem de uma instância Vector<T> é fixa para o tempo de vida de um aplicativo, mas seu valor Vector<T>.Count depende da CPU do computador que executa o código.

    Observação

    O tipo Vector<T> não está incluído no .NET Framework. Você deve instalar o pacote System.Numerics.Vectors do NuGet para obter acesso a esse tipo.

Os tipos acelerados por SIMD são implementados de modo que possam ser usados com hardware não acelerados por SIMD ou compiladores JIT. Para aproveitar as instruções SIMD, os aplicativos de 64 bits deverão ser executados pelo runtime que utiliza o compilador RyuJIT. Um compilador RyuJIT é incluído no .NET Core e no .NET Framework 4.6 e posteriores. O suporte a SIMD só é fornecido ao direcionar processadores de 64 bits.

Como usar o SIMD?

Antes de executar algoritmos SIMD personalizados, é possível verificar se o computador host dá suporte a SIMD usando Vector.IsHardwareAccelerated, o que retorna um Boolean. Isso não garante que a aceleração de SIMD esteja habilitada para um tipo específico, mas é um indicador de que ela é compatível com alguns tipos.

Vetores simples

Os tipos mais primitivos acelerados por SIMD no .NET são tipos Vector2, Vector3 e Vector4, que representam vetores com valores Single 2, 3 e 4. O exemplo a seguir usa Vector2 para adicionar dois vetores.

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

Também é possível usar vetores .NET para calcular outras propriedades matemáticas de vetores, como Dot product, Transform, Clamp etc.

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 uma matriz 3x2, e Matrix4x4, que representa uma matriz 4x4. Pode ser usado para cálculos relacionados à matriz. O exemplo a seguir demonstra a multiplicação de uma matriz para sua matriz de transposição correspondente usando 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>

O Vector<T> permite usar vetores mais longos. A contagem de uma instância Vector<T> é corrigida, mas seu valor Vector<T>.Count depende da CPU do computador que executa o código.

O exemplo a seguir demonstra como calcular a soma em termos de elemento de duas matrizes usando 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;
}

Comentários

É mais provável que o SIMD remova um gargalo e exponha o próximo, por exemplo, a taxa de transferência de memória. Em geral, o benefício de desempenho do uso do SIMD varia conforme o cenário específico e, em alguns casos, pode até mesmo ter um desempenho pior do que o código equivalente não SIMD mais simples.