Usare tipi numerici con accelerazione SIMD

SIMD (Single Instruction Multiple Data) fornisce supporto hardware per l'esecuzione di un'operazione su più dati, in parallelo, usando una singola istruzione. In .NET è disponibile un set di tipi con accelerazione SIMD nello spazio dei nomi System.Numerics. Le operazioni SIMD possono essere parallelizzate a livello di hardware. Questo approccio aumenta la velocità effettiva dei calcoli vettorializzati, che sono comuni in app matematiche scientifiche e grafiche.

Tipi con accelerazione SIMD .NET

I tipi con accelerazione SIMD .NET includono i seguenti tipi:

  • Tipi Vector2, Vector3 e Vector4, che rappresentano i vettori con 2, 3 e 4 valori Single.

  • Due tipi matrice: Matrix3x2, che rappresenta una matrice 3x2, e Matrix4x4, che rappresenta una matrice 4x4 di valori Single.

  • Tipo Plane, che rappresenta un piano nello spazio tridimensionale utilizzando valori Single.

  • Tipo Quaternion, che rappresenta un vettore usato per codificare le rotazioni fisiche tridimensionali utilizzando valori Single.

  • Tipo Vector<T>, che rappresenta un vettore di un tipo numerico specificato e fornisce un ampio set di operatori che traggono vantaggio dal supporto per SIMD. Il conteggio di un'istanza di Vector<T> è fisso per la durata di un'applicazione, ma il relativo valore Vector<T>.Count dipende dalla CPU del computer che esegue il codice.

    Nota

    Il tipo Vector<T> non è incluso in .NET Framework. È necessario installare il pacchetto NuGet System.Numerics.Vectors per ottenere l'accesso a questo tipo.

I tipi con accelerazione SIMD vengono implementati in modo da poter essere usati con hardware senza accelerazione SIMD o compilatori JIT. Per sfruttare le istruzioni SIMD, le app a 64 bit devono essere eseguite dal runtime che usa il compilatore RyuJIT. Un compilatore RyuJIT è incluso in .NET Core e in .NET Framework 4.6 e versioni successive. Il supporto di SIMD viene fornito solo quando le destinazioni sono processori a 64 bit.

Come usare SIMD

Prima di eseguire algoritmi SIMD personalizzati, è possibile verificare se il computer host supporta SIMD usando Vector.IsHardwareAccelerated, che restituisce un valore Boolean. Ciò non garantisce che l'accelerazione SIMD sia abilitata per un tipo specifico, ma indica che è supportata da alcuni tipi.

Vettori semplici

I tipi con accelerazione SIMD più primitivi in .NET sono i tipi Vector2, Vector3 e Vector4, che rappresentano vettori con 2, 3 e 4 valori Single. L'esempio seguente usa Vector2 per aggiungere due vettori.

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

È anche possibile usare vettori .NET per calcolare altre proprietà matematiche dei vettori, ad esempio, Dot product, Transform, Clamp e così via.

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

Con matrice

Matrix3x2, che rappresenta una matrice 3x2, e Matrix4x4, che rappresenta una matrice 4x4. Possono essere usati per i calcoli correlati alla matrice. L'esempio seguente illustra la moltiplicazione di una matrice per la matrice trasposta corrispondente tramite 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> offre la possibilità di usare vettori più lunghi. Il conteggio di un'istanza di Vector<T> è fisso, ma il suo valore Vector<T>.Count dipende dalla CPU del computer che esegue il codice.

Nell'esempio seguente viene illustrato come calcolare la somma di due matrici dal punto di vista di un elemento 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;
}

Osservazioni:

È più probabile che SIMD elimini un collo di bottiglia ed evidenzi quello successivo, ad esempio la velocità effettiva della memoria. In generale, il vantaggio dell'uso di SIMD per le prestazioni varia in base allo scenario specifico e in alcuni casi può persino comportare prestazioni peggiori rispetto al codice equivalente non SIMD più semplice.