System.Numerics.Vector <T> Initialisierungsleistung unter .NET Framework

Nov 07 2020

System.Numerics.Vector bietet SIMD-Unterstützung für .NET Core und .NET Framework. Es funktioniert unter .NET Framework 4.6+ und .NET Core.

// Baseline
public void SimpleSumArray() 
{
    for (int i = 0; i < left.Length; i++)
        results[i] = left[i] + right[i];
}

// Using Vector<T> for SIMD support
public void SimpleSumVectors() 
{
    int ceiling = left.Length / floatSlots * floatSlots;
    
    for (int i = 0; i < ceiling; i += floatSlots)
    {
        Vector<float> v1 = new Vector<float>(left, i);
        Vector<float> v2 = new Vector<float>(right, i);
        (v1 + v2).CopyTo(results, i);
    }
    for (int i = ceiling; i < left.Length; i++)
    {
        results[i] = left[i] + right[i];
    }
}

Leider kann die Initialisierung des Vektors der begrenzende Schritt sein. Um dies zu umgehen, empfehlen mehrere Quellen die Verwendung von MemoryMarshal, um das Quellarray in ein Array von Vektoren umzuwandeln [1] [2]. Zum Beispiel:

// Improving Vector<T> Initialization Performance
public void SimpleSumVectorsNoCopy() 
{
    int numVectors = left.Length / floatSlots;
    int ceiling = numVectors * floatSlots;
    // leftMemory is simply a ReadOnlyMemory<float> referring to the "left" array
    ReadOnlySpan<Vector<float>> leftVecArray = MemoryMarshal.Cast<float, Vector<float>>(leftMemory.Span);
    ReadOnlySpan<Vector<float>> rightVecArray = MemoryMarshal.Cast<float, Vector<float>>(rightMemory.Span);
    Span<Vector<float>> resultsVecArray = MemoryMarshal.Cast<float, Vector<float>>(resultsMemory.Span);
    for (int i = 0; i < numVectors; i++)
        resultsVecArray[i] = leftVecArray[i] + rightVecArray[i];
}

Dies führt zu einer dramatischen Leistungsverbesserung unter .NET Core :

|                 Method |      Mean |     Error |    StdDev |
|----------------------- |----------:|----------:|----------:|
|         SimpleSumArray | 165.90 us | 0.1393 us | 0.1303 us |
|       SimpleSumVectors |  53.69 us | 0.0473 us | 0.0443 us |
| SimpleSumVectorsNoCopy |  31.65 us | 0.1242 us | 0.1162 us |

Leider hat diese Art der Initialisierung des Vektors in .NET Framework den gegenteiligen Effekt. Es führt tatsächlich zu einer schlechteren Leistung:

|                 Method |      Mean |    Error |   StdDev |
|----------------------- |----------:|---------:|---------:|
|         SimpleSumArray | 152.92 us | 0.128 us | 0.114 us |
|       SimpleSumVectors |  52.35 us | 0.041 us | 0.038 us |
| SimpleSumVectorsNoCopy |  77.50 us | 0.089 us | 0.084 us |

Gibt es eine Möglichkeit, die Initialisierung von Vector unter .NET Framework zu optimieren und eine ähnliche Leistung wie .NET Core zu erzielen? Mit dieser Beispielanwendung wurden Messungen durchgeführt [1].

[1] https://github.com/CBGonzalez/SIMDPerformance

[2] https://stackoverflow.com/a/62702334/430935

Antworten

2 harold Nov 08 2020 at 10:18

Soweit ich weiß, ist die einzige effiziente Möglichkeit, einen Vektor in .NET Framework 4.6 oder 4.7 zu laden (vermutlich ändert sich dies alles in 5.0), unsicherer Code, beispielsweise mit Unsafe.Read<Vector<float>>(oder seiner nicht ausgerichteten Variante, falls zutreffend):

public unsafe void SimpleSumVectors()
{
    int ceiling = left.Length / floatSlots * floatSlots;

    fixed (float* leftp = left, rightp = right, resultsp = results)
    {
        for (int i = 0; i < ceiling; i += floatSlots)
        {
            Unsafe.Write(resultsp + i, 
                Unsafe.Read<Vector<float>>(leftp + i) + Unsafe.Read<Vector<float>>(rightp + i));
        }
    }
    for (int i = ceiling; i < left.Length; i++)
    {
        results[i] = left[i] + right[i];
    }
}

Dies verwendet das System.Runtime.CompilerServices.UnsafePaket, das Sie über NuGet erhalten können, aber es könnte auch ohne dies erfolgen.