SIMD en hardware-intrinsics gebruiken in .NET

SIMD (één instructie, meerdere gegevens) is hardwareondersteuning voor het toepassen van één bewerking op meerdere stukjes gegevens parallel met één instructie. Vectorized code verwerkt verschillende waarden per iteratie in plaats van één, waardoor de doorvoer voor het soort numerieke, wetenschappelijke, grafische, tekstverwerking en gegevensparallel werk aanzienlijk kan toenemen, waarbij dezelfde bewerking wordt herhaald via een buffer. De keerzijde is extra complexiteit, dus dit loont vooral wanneer de input groot genoeg is en met metingen is bevestigd dat het daadwerkelijk winst oplevert.

.NET biedt verschillende soorten SIMD-ondersteuning. Kies degene die overeenkomt met de hoeveelheid controle die u nodig hebt en hoeveel complexiteit u wilt overnemen.

Typen SIMD-ondersteuning in .NET

API (Application Programming Interface) Namespace Wanneer gebruikt u het?
Vector- en matrixtypen met vaste doeleinden System.Numerics Grafische en meetkundige wiskunde met vectoren met 2 tot 4 elementen, matrices, quaternionen en vlakken.
Vector<T> System.Numerics Draagbare vectorisatie met variabele breedte wanneer u geen controle per platform nodig hebt.
Vector64<T>,Vector128<T>,Vector256<T>,Vector512<T> System.Runtime.Intrinsics Platformoverschrijdende vectorisatie met vaste breedte met fijnmazige controle. Dit is het aanbevolen startpunt voor nieuwe vectorized algoritmen.
Hardware-intrinsieke kenmerken System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm, System.Runtime.Intrinsics.Wasm Specifieke processorinstructies die niet beschikbaar zijn via API's op een hoger niveau, voor die allerlaatste prestatiewinst in een kritisch codepad.
TensorPrimitives System.Numerics.Tensors Gebruiksklare, gevectoriseerde berekeningen voor spans. Het doet de vectorisatie voor u.

Wanneer deze API's elkaar overlappen, hebben ze betrekking op lagen van abstractie. De generieke vectortypen zijn de fundamentele uitwisselingstypen die door de andere lagen worden doorgegeven, dus bevinden ze zich technisch gezien op het laagste niveau: de vector met variabele breedte, Vector<T>, die groeit tot de breedte die door de gebruikte hardware wordt ondersteund, en de typen met vaste breedte, Vector64<T> tot en met Vector512<T>. De platformspecifieke hardware-intrinsics in System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm en System.Runtime.Intrinsics.Wasm werken met deze typen, waarbij elk daarvan rechtstreeks overeenkomt met een afzonderlijke processorinstructie. De platformonafhankelijke bewerkingen die beschikbaar zijn op de generieke typen, bevinden zich een niveau boven de platformspecifieke intrinsieke functies en worden voor elk doelplatform daarnaar teruggebracht. Hoger zijn nog steeds de beheerde API's die worden uitgevoerd via volledige buffers, gevectoriseerde methoden op Span<T> en string, en TensorPrimitivesdie op de onderstaande lagen bouwen, zodat u SIMD-versnelling krijgt zonder deze handmatig te schrijven. De System.Numerics typen met een vaste vorm zijn domeinspecifieke handige typen voor grafische toepassingen en geometrie en maken geen deel uit van deze uitwisselingsstack.

De rest van dit artikel werkt via deze API's van het hoogste tot het laagste niveau, en behandelt vervolgens testen, benchmarken en best practices.

System.Numerics vector- en matrixtypen

De System.Numerics naamruimte biedt SIMD-versnelde typen met een vaste vorm:

Deze typen zijn van nature toegewezen aan afbeeldingen en geometrie, en de runtime versnelt hun bewerkingen met SIMD-instructies waar de hardware ze ondersteunt. In het volgende voorbeeld worden twee vectoren toegevoegd:

Vector2 v1 = Vector2.Create(0.1f, 0.2f);
Vector2 v2 = Vector2.Create(1.1f, 2.2f);
Vector2 sum = v1 + v2;

Ze geven ook de algemene vectorberekening weer die u zou verwachten, zoals puntproduct, afstand en klem:

float dot = Vector2.Dot(v1, v2);
float distance = Vector2.Distance(v1, v2);
Vector2 clamped = Vector2.Clamp(v1, Vector2.Zero, Vector2.One);

De matrixtypen ondersteunen matrixberekeningen, zoals transponeren en vermenigvuldigen:

Matrix4x4 m1 = Matrix4x4.Create(
    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);

Matrix4x4 m2 = Matrix4x4.Transpose(m1);
Matrix4x4 product = Matrix4x4.Multiply(m1, m2);

Vector<T>

Vector<T> vertegenwoordigt een vector met variabele breedte van een primitief numeriek type. De lengte ervan is vast voor de levensduur van het proces, maar de waarde is afhankelijk van Vector<T>.Count de CPU die de code uitvoert. De Just-In-Time-compiler (JIT) behandelt Count als een constante, dus lussen die hiervan gebruikmaken worden goed geoptimaliseerd.

Vector<T> biedt u draagbare vectorisatie zonder platformcode, ten koste van het niet kennen van de vectorbreedte tijdens het compileren. In het volgende voorbeeld wordt de elementsgewijze optelling van twee arrays berekend:

// Illustrative: element-wise add with Vector<T>. In practice, prefer the already-accelerated
// TensorPrimitives.Add, which is optimized for every Vector<T>.IsSupported element type.
public static double[] Add(double[] left, double[] right)
{
    ArgumentNullException.ThrowIfNull(left);
    ArgumentNullException.ThrowIfNull(right);
    ArgumentOutOfRangeException.ThrowIfNotEqual(right.Length, left.Length);

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

    int i = 0;

    // Vector<T>.Count is a JIT-time constant, so the compiler optimizes the loop bound.
    int lastVectorStart = left.Length - Vector<double>.Count;

    for (; i <= lastVectorStart; i += Vector<double>.Count)
    {
        Vector<double> v1 = Vector.Create(left.AsSpan(i));
        Vector<double> v2 = Vector.Create(right.AsSpan(i));
        (v1 + v2).CopyTo(result, i);
    }

    // Process any remaining elements that don't fill a full vector.
    // Simplified for illustration: a scalar tail isn't optimal. A vectorized
    // remainder that reprocesses the last full vector avoids the per-element loop.
    for (; i < left.Length; i++)
    {
        result[i] = left[i] + right[i];
    }

    return result;
}

Opmerking

Dit voorbeeld is illustratief. Je hoeft zelden zelf een lus als deze te schrijven, omdat TensorPrimitives al versnelde, op spans gebaseerde rekenfunctionaliteit biedt. Deze elementsgewijze optelling is Add, en reducties zoals Sum zijn ook beschikbaar. Deze bewerkingen zijn hardwareversneld voor de elementtypen die Vector<T> ondersteuning bieden (Vector<T>.IsSupported).

Controleren of hardwareversnelling beschikbaar is

SimD-versnelde typen werken zelfs op hardware- of JIT-configuraties die geen ondersteuning bieden voor SIMD, omdat ze terugvallen op niet-versnelde software-implementaties. Om te bepalen of versnelling daadwerkelijk beschikbaar is, controleert u de betreffende IsHardwareAccelerated-eigenschap:

Deze eigenschappen worden door de JIT omgezet in constanten, dus de vertakkingen die u niet neemt, worden geëlimineerd en er zijn geen runtimekosten om ze te controleren. Sla de waarden niet in de cache op; lees ze rechtstreeks waar u ze nodig hebt. Hetzelfde geldt voor de Count eigenschappen (bijvoorbeeld Vector128<T>.Count), die ook JIT-tijdconstanten zijn.

De meeste bewerkingen op een versnelde breedte worden zelf versneld, maar het is niet gegarandeerd voor elke bewerking. Zo kan bijvoorbeeld deling met drijvende komma's worden versneld, terwijl deling met gehele getallen dat niet wordt. Wanneer Vector256 wordt versneld, Vector128 meestal ook, maar er is geen garantie, dus controleer elke breedte die u gebruikt.

Tip

Als een bewerking die u nodig hebt, niet wordt versneld op een platform dat u belangrijk vindt, of als u een nieuwe platformoverschrijdende API wilt, kunt u een probleem indienen op dotnet/runtime. Hetzelfde geldt voor verbeteringen in codegen.

Niet elk elementtype is geldig voor elke vector. Vector128<T> en verwante typen ondersteunen momenteel de primitieve numerieke typen (byte, sbyte, short, ushort, int, uint, long, ulong, float, double, nint en nuint), en die verzameling kan in de toekomst worden uitgebreid met andere typen. Gebruik Vector128<T>.IsSupported om te bepalen of een gegeven T geldig is, wat vooral handig is voor generieke code.

Typen die niet worden ondersteund, zoals char en bool, kunnen nog steeds worden gevectoriseerd door de buffer opnieuw te interpreteren als een ondersteund type van dezelfde grootte. Gebruik Cast om een span opnieuw te interpreteren, bijvoorbeeld char als ushort, of de methode As<TFrom, TTo> van de vector om een vector die u al hebt opnieuw te interpreteren. Met herinterpretatie wordt alleen het type gewijzigd, niet de onderliggende bits, dus het is uw verantwoordelijkheid om de gegevens goed gevormd te houden: een bool must blijft 0 of 1, en een char moet een geldige UTF-16-code-eenheid blijven. Als een gevectoriseerde bewerking een buiten bereikwaarde kan produceren, moet u het resultaat normaliseren voordat u deze terug schrijft.

Platformoverschrijdende vectorisatie met Vector128

Vector128<T> is de gemeenschappelijke noemer voor elk platform dat vectorisatie ondersteunt, dus het is de beste plek om te beginnen. Het bevat een 128-bits vector: 16 bytes, 8 shorts, 4 ints/floats of 2 longs/doubles.

------------------------------128-bits---------------------------
|             64                |               64              |
-----------------------------------------------------------------
|      32       |      32       |      32       |      32       |
-----------------------------------------------------------------
|  16   |  16   |  16   |  16   |  16   |  16   |  16   |  16   |
-----------------------------------------------------------------
| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
-----------------------------------------------------------------

Vector256<T> is twee keer zo breed en Vector512<T> twee keer opnieuw. Niet alle hardware ondersteunt de grotere breedtes, dus de volgende voorbeelden gebruiken Vector128 omwille van de overdraagbaarheid.

Elke breedte heeft een algemeen type (Vector128<T>) voor de gegevens en een niet-algemene statische klasse (Vector128) die de meeste bewerkingen bevat, waaronder statische factory-methoden zoals Create en Load. Operators zoals +, &en << zijn de idiomatische manier om rekenkundige en bitbewerkingen uit te drukken; geef ze de voorkeur boven de benoemde methode-equivalenten om fouten met operatorprioriteit te voorkomen en de leesbaarheid te verbeteren. Voor algoritmen die afhankelijk zijn van bytevolgorde, vertak op IsLittleEndian, die de JIT ook tot een constante vereenvoudigt.

Opmerking

Op x86/x64 Vector256<T> worden bewerkingen over het algemeen behandeld als twee onafhankelijke 128-bits "lanes". Voor de meeste elementgewijze bewerkingen is dit transparant, maar bewerkingen die lanen doorkruisen (zoals shuffles of paargewijze/horizontale bewerkingen) kunnen zich mogelijk anders gedragen of meer kosten dan het equivalent van Vector128. Bevestig met benchmarks voordat u ervan uitgaat dat een bredere vector sneller is.

Baankruisingsoperaties worden niet gratis uitgebreid

Elementgewijze bewerkingen maken zich niet druk om de breedte: v1 + v2 geeft hetzelfde resultaat per element, ongeacht of v1 en v2Vector128<T> of Vector256<T> zijn — verbreden verwerkt per instructie alleen de hoeveelheid gegevens van één extra lane. Add op v = [a, b, c, d] en w = [e, f, g, h] combineert altijd elementen met dezelfde index:

v: [ a | b | c | d ]
w: [ e | f | g | h ]
     +   +   +   +
r: [a+e|b+f|c+g|d+h]

Bewerkingen waarbij lanes worden gekruist schalen niet zo eenvoudig op, omdat welke elementen worden gecombineerd afhangt van de breedte van de vector. Een paargewijze reductie combineert aangrenzende elementen in plaats van elementen met dezelfde index, zodat verbreding verandert welke elementen uiteindelijk samen worden gepaard:

v:       [  a  |  b  |  c  |  d  ]
            \_____/     \_____/
round 1: [ a+b | c+d | a+b | c+d ]
            \_________________/
round 2: [  S  |  S  |  S  |  S  ]   (S = a+b+c+d)

Dat is precies wat een horizontale reductie doet: de elementen van een vector optellen met twee rondes van paargewijze optellen. Op x86/x64 komt u daar met Vector128<float> (4 elementen) met twee aanroepen van HorizontalAdd:

// Sums all four elements with two rounds of pairwise horizontal adds.
// HorizontalAdd(v, v) on [a, b, c, d] gives [a+b, c+d, a+b, c+d]; a second round
// collapses that to the full sum in every element.
public static float SumVector128(Vector128<float> v)
{
    Debug.Assert(Sse3.IsSupported);

    Vector128<float> step1 = Sse3.HorizontalAdd(v, v);
    Vector128<float> step2 = Sse3.HorizontalAdd(step1, step1);

    return step2.ToScalar();
}

Breid hetzelfde patroon met twee aanroepen uit naar Vector256<float> (8 elementen) en het ziet er goed uit, dit is niet het juiste. HorizontalAdd werkt niet over de gehele 256-bits vector; hiermee wordt het paargewijze patroon onafhankelijk binnen elke 128-bits baan herhaald. Na twee rondes krijgt u de som van de onderste helft (elementen 0-3), gerepliceerd in de onderste helft, en de som van de bovenste helft (elementen 4-7), gerepliceerd in de bovenste helft, niet de som van alle acht elementen:

// The same two-round pattern on Vector256<float> looks like it should sum all eight
// elements, but Avx.HorizontalAdd repeats the pairwise pattern independently within
// each 128-bit lane. The result holds the lower lane's sum (elements 0-3) broadcast
// across the lower lane and the upper lane's sum (elements 4-7) broadcast across the
// upper lane -- ToScalar only returns the lower lane's partial sum, not the total.
public static float SumVector256Naive(Vector256<float> v)
{
    Debug.Assert(Avx.IsSupported);

    Vector256<float> step1 = Avx.HorizontalAdd(v, v);
    Vector256<float> step2 = Avx.HorizontalAdd(step1, step1);

    return step2.ToScalar();
}

Als u het juiste totaal wilt ophalen, moet u de baangrens expliciet overbrugden: lees de gedeeltelijke som van elke rijstrook met GetLower/GetUpper en voeg ze samen toe enGetLowerGetUpper splitst een vector in de eerste en tweede helft:

------------------------------128-bits---------------------------
|           LOWER               |             UPPER             |
-----------------------------------------------------------------
|      32       |      32       |      32       |      32       |
-----------------------------------------------------------------
|  16   |  16   |  16   |  16   |  16   |  16   |  16   |  16   |
-----------------------------------------------------------------
| 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
-----------------------------------------------------------------
// Getting the full sum needs an explicit step to cross the lane boundary: read each
// lane's partial sum out with GetLower/GetUpper and add them together.
public static float SumVector256(Vector256<float> v)
{
    Debug.Assert(Avx.IsSupported);

    Vector256<float> step1 = Avx.HorizontalAdd(v, v);
    Vector256<float> step2 = Avx.HorizontalAdd(step1, step1);

    Vector128<float> lower = step2.GetLower();
    Vector128<float> upper = step2.GetUpper();

    return lower.ToScalar() + upper.ToScalar();
}

Die extra stap is de werkelijke kosten van kruisingsbanen. Een lane-overschrijdend algoritme wordt niet zomaar kosteloos breder zoals een elementgewijs algoritme dat wel wordt—meet eerst voordat je aanneemt dat de bredere vector wint.

Algemene bewerkingen

Vector128 en de grotere verwante varianten bieden een uitgebreid API-oppervlak. U hoeft deze niet te onthouden: ken de categorieën en zoek de details op wanneer u ze nodig hebt. Elke bewerking heeft een softwareterugval voor platforms die deze niet kunnen versnellen. In de volgende tabel wordt in feite het hele oppervlak behandeld.

Categorie Wat het doet Representatieve API's
Constants Vooraf gedefinieerde constante vectoren Zero, One, NegativeOne, AllBitsSet, Indices, SignSequence, E, Pi, Tau, Epsilon, NaN, PositiveInfinity, NegativeInfinity, NegativeZero
Oprichting Een scalaire waarde broadcasten, elementen instellen of een reeks genereren Create, CreateScalar, CreateScalarUnsafe, Create(ReadOnlySpan<T>), CreateSequence, CreateGeometricSequence, , CreateHarmonicSequence, CreateAlternatingSequence
Laden en opslaan Gegevens verplaatsen tussen het geheugen en een vector Load, LoadUnsafe, LoadAligned, LoadAlignedNonTemporal, Store, StoreUnsafe, StoreAligned, StoreAlignedNonTemporal, CopyTo, TryCopyTo
Arithmetic Elementgewijs wiskunde en reducties Add (x + y), Subtract (x - y), Multiply (x * y), Divide (x / y), Negate (-x), AddSaturate, SubtractSaturate, Abs, Sqrt, FusedMultiplyAdd, Dot, Sum
Bitbewerkingen Bitsgewijze logica en verschuivingen BitwiseAnd (x & y), BitwiseOr (x \| y), Xor (x ^ y), AndNot (x & ~y), OnesComplement (~x), ShiftLeft (x << n), , ShiftRightArithmetic (x >> n), ShiftRightLogical (x >>> n)
Min, max en klem Elementgewijze minimum-, maximum- en bereikklem Min, Max, Clamp, MinMagnitude, MaxMagnitude, MinNumber, MaxNumber, MinMagnitudeNumber, MaxMagnitudeNumber
Afronding Elk element afronden op een integrale waarde Ceiling,Floor,Round,Truncate
Wiskundige functies Teken-, interpolatie-, hoek- en transcendentale hulpfuncties CopySign, Lerp, DegreesToRadians, RadiansToDegrees, Hypot, Sin, Cos, SinCos, Asin, Exp, Log, Log2
Comparison Vergelijk elk element; het resultaat is een vectormasker, niet wat de bool-operator oplevert Equals, GreaterThan, GreaterThanOrEqual, LessThan, , LessThanOrEqual
Classification Predicaten per element op de getallenlijn, die elk een vectormasker retourneren IsNaN, IsFinite, IsInfinity, IsPositiveInfinity, IsNegativeInfinity, IsInteger, IsEvenInteger, IsOddInteger, IsNegative, IsPositive, IsNormal, IsSubnormal, IsZero
Vergelijkingsverminderingen Een vergelijking per element samenvouwen tot één enkele vergelijking bool EqualsAll (x == y), EqualsAny, GreaterThanAll, GreaterThanAny, GreaterThanOrEqualAll, GreaterThanOrEqualAny, LessThanAll, LessThanAny, LessThanOrEqualAll, LessThanOrEqualAny
Predicaten voor hele vectoren Reduceer een vector tot een bool: of alle, enige of geen elementen gelijk zijn aan een waarde, of (de WhereAllBitsSet-varianten) alle bits ingesteld hebben. Geef hieraan de voorkeur boven het converteren van een masker naar een index All, Any, None, AllWhereAllBitsSet, AnyWhereAllBitsSet, NoneWhereAllBitsSet
Search Elementen tellen of lokaliseren op basis van waarde, of (de WhereAllBitsSet varianten) posities in een masker instellen Count, IndexOf, LastIndexOf, CountWhereAllBitsSet, IndexOfWhereAllBitsSet, LastIndexOfWhereAllBitsSet
Masker om te indexeren Een vergelijkingsmasker omzetten in een scalaire bitmasker en deze scannen ExtractMostSignificantBits met TrailingZeroCount of LeadingZeroCount
Selectie Twee vectoren mengen volgens een masker, bit voor bit ConditionalSelect(x, y, z), gelijk aan (y & x) \| (z & ~x)
Conversion Numeriek type wijzigen, nieuwe waarden berekenen (bijvoorbeeld int in float) ConvertToInt32, ConvertToInt64, ConvertToUInt32, ConvertToUInt64, ConvertToSingle, ConvertToDouble
Verbreden en versmallen Elementen splitsen in een breder type of inpakken in een smallere Widen, WidenLower, WidenUpper, Narrow, , NarrowWithSaturation
Herinterpretatie De bits opnieuw interpreteren als een ander elementtype zonder ze te wijzigen As<TFrom, TTo>, AsByte, AsInt32, AsSingle en de andere As*-elementvormen
System.Numerics-interop Opnieuw interpreteren tussen Vector128<T> en de numerieke typen met vaste vorm AsVector, AsVector2, AsVector3, AsVector4, AsPlane, AsQuaternion, , AsVector128, AsVector128Unsafe
Bestellen Elementen opnieuw rangschikken op index of twee vectoren tussen elkaar opslaan Shuffle, Reverse, Zip, ZipLower, ZipUpper, Unzip, UnzipEven, UnzipOdd, ConcatLowerLower, ConcatLowerUpper, ConcatUpperLower, ConcatUpperUpper
Toegang tot rijstrook Afzonderlijke elementen en halveren lezen of vervangen of het formaat van de vector wijzigen GetElement, WithElement, ToScalar, GetLower, GetUpper, WithLower, , WithUpper, ToVector256

Tip

Verschillende bewerkingen hebben varianten van Estimate en Native — bijvoorbeeld MultiplyAddEstimate, ClampNative, MinNative, MaxNative, ShuffleNative en ConvertToInt32Native. Ze komen overeen met een snellere hardware-instructie die wat precisie inlevert of een IEEE-garantie voor randgevallen (zoals NaN-afhandeling) opgeeft, dus gebruik ze alleen wanneer uit een benchmark blijkt dat juist de exacte vorm de bottleneck is en de minder strikte semantiek acceptabel is.

Opmerking

Vector256.Shuffle behandelt de invoer als één 256-bits vector, waarbij het platformspecifieke Avx2.Shuffle werkt als twee onafhankelijke 128-bits banen. De platformoverschrijdende API is de meer draagbare keuze, maar bevestig het gedrag dat u nodig hebt bij het overzetten van handgeschreven intrinsieken.

Het codepad structuren

Een gevectoriseerde methode splitst zich meestal op in een codepad per vectorbreedte, plus een scalaire terugvalroute voor kleine invoerwaarden en hardware zonder versnelling. Als u de grootste vector wilt gebruiken die door de hardware wordt ondersteund, controleert u eerst de breedste vector en werkt u af:

// Sums a buffer, choosing the widest vector the hardware and element type support.
public static T Sum<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    // The widest-first order continues with the Vector512 and Vector256 paths, which belong
    // here ahead of the Vector128 block below. They're identical to it aside from the wider
    // type (for example, Vector512<T> with Vector512.Create and Vector512.Sum), so they're
    // omitted for brevity:
    //
    // if (Vector512.IsHardwareAccelerated && Vector512<T>.IsSupported)
    // {
    //     if (buffer.Length >= Vector512<T>.Count)
    //     {
    //         return SumVector512(buffer);
    //     }
    //     return SumVectorSmall(buffer);
    // }
    //
    // if (Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported)
    // {
    //     if (buffer.Length >= Vector256<T>.Count)
    //     {
    //         return SumVector256(buffer);
    //     }
    //     return SumVectorSmall(buffer);
    // }

    if (Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported)
    {
        if (buffer.Length >= Vector128<T>.Count)
        {
            return SumVector128(buffer);
        }
        return SumVectorSmall(buffer);
    }

    return SumScalar(buffer);
}

De buitenbeveiliging van elke breedte combineert Vector128.IsHardwareAccelerated (een JIT-tijdconstante om te bepalen of het platform die breedte versnelt) met Vector128<T>.IsSupported (of het elementtype T geldig is voor die breedte). Vergelijk binnen een ondersteund blok de invoerlengte met Count om te kiezen tussen het gevectoriseerde codepad en een fallback voor kleine invoer. De methode is generiek voor T, en de blokken Vector256 en Vector512 — identiek aan het blok Vector128, maar met het bredere type — worden omwille van de beknoptheid als commentaar weergegeven.

Er zijn twee verschillende fallbacks. Een buffer die te klein is voor zelfs de smalste vector, maar op versnelde hardware gaat naar SumVectorSmall: een expliciete switch jumptabel die elke mogelijke subvectorlengte zonder lus afhandelt:

// Sums a buffer smaller than the widest vector. The complete "optimal" shape dispatches on the
// element width so each width uses a switch jump table sized to the number of elements that fit
// in the widest vector (63 for byte, 31 for short, 15 for int/float, 7 for long/double).
private static T SumVectorSmall<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    // sizeof(T) is a JIT constant, so only the matching branch survives for a given T.
    if (sizeof(T) == 4)
    {
        return SumVectorSmall4(buffer);
    }

    // The 1-, 2-, and 8-byte tables share the shape below, sized for their element width.
    // They're omitted for brevity, so those widths fall back to a scalar loop here:
    //
    // if (sizeof(T) == 1) return SumVectorSmall1(buffer); // switch over lengths 0..63
    // if (sizeof(T) == 2) return SumVectorSmall2(buffer); // switch over lengths 0..31
    // if (sizeof(T) == 8) return SumVectorSmall8(buffer); // switch over lengths 0..7
    return SumScalar(buffer);
}

private static T SumVectorSmall4<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(sizeof(T) == 4);
    Debug.Assert(buffer.Length < Vector512<T>.Count);

    T result = T.Zero;

    // A 4-byte element gives Count == 4/8/16 for Vector128/256/512, so a remainder can be up to
    // 15 elements. The larger cases fold the leftover with the widest vector that fits, using two
    // overlapping loads (one from the start, one from the end) rather than recursing—the shape
    // TensorPrimitives uses. The loads overlap for lengths that aren't an exact multiple of the
    // width, so the tail is masked down to the additive identity before it's summed. That mask is
    // only needed because addition is non-idempotent; an idempotent operation such as a search
    // could fold the overlapping tail in directly.
    switch (buffer.Length)
    {
        // One or two Vector256's worth of data.
        case 15:
        case 14:
        case 13:
        case 12:
        case 11:
        case 10:
        case 9:
        case 8:
        {
            Vector256<T> beg = Vector256.Create(buffer);
            Vector256<T> end = Vector256.Create(buffer.Slice(buffer.Length - Vector256<T>.Count));

            Vector256<T> msk = CreateRemainderMask256<T>(buffer.Length - Vector256<T>.Count);
            end = Vector256.ConditionalSelect(msk, end, Vector256<T>.Zero);

            result = Vector256.Sum(beg + end);
            break;
        }

        // One or two Vector128's worth of data.
        case 7:
        case 6:
        case 5:
        case 4:
        {
            Vector128<T> beg = Vector128.Create(buffer);
            Vector128<T> end = Vector128.Create(buffer.Slice(buffer.Length - Vector128<T>.Count));

            Vector128<T> msk = CreateRemainderMask128<T>(buffer.Length - Vector128<T>.Count);
            end = Vector128.ConditionalSelect(msk, end, Vector128<T>.Zero);

            result = Vector128.Sum(beg + end);
            break;
        }

        // Smaller than a single vector: each case falls through to the next, accumulating one
        // element per label.
        case 3:
        {
            result += buffer[2];
            goto case 2;
        }

        case 2:
        {
            result += buffer[1];
            goto case 1;
        }

        case 1:
        {
            result += buffer[0];
            goto case 0;
        }

        case 0:
        {
            break;
        }
    }

    return result;
}

// Builds a mask whose last `keepLast` lanes are all-bits-set and the rest zero, so an overlapping
// tail load can be folded in without double-counting the lanes the head already covered.
// TensorPrimitives uses an internal table-based helper. The mask is only a bit pattern keyed on
// lane width, so it's built with the same-width integer Indices ([0, 1, 2, ...]) and reinterpreted
// to T: integer comparisons are cheaper than floating-point ones, so a float/double table would
// still compare as int/long rather than in its own element type.
private static Vector256<T> CreateRemainderMask256<T>(int keepLast)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(sizeof(T) == 4);

    Vector256<int> firstKept = Vector256.Create(Vector256<int>.Count - keepLast);
    return Vector256.GreaterThanOrEqual(Vector256<int>.Indices, firstKept).As<int, T>();
}

private static Vector128<T> CreateRemainderMask128<T>(int keepLast)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(sizeof(T) == 4);

    Vector128<int> firstKept = Vector128.Create(Vector128<int>.Count - keepLast);
    return Vector128.GreaterThanOrEqual(Vector128<int>.Indices, firstKept).As<int, T>();
}

sizeof(T) is ook een constante tijdens JIT-compilatie, dus SumVectorSmall kiest op basis van de elementbreedte een tabel die is gedimensioneerd voor het aantal elementen dat in de breedste vector past—dezelfde aanpak die TensorPrimitives gebruikt. (Wanneer het nieuwe model voor geheugenveiligheid is ingeschakeld, sizeof(T) worden expressies voor een typeparameter met de unmanaged beperking toegestaan in veilige code.) Alleen de tabel met vier bytes wordt weergegeven; De tabellen 1-byte, 2-byte en 8-byte delen de vorm. In de grotere gevallen vouwen die het restant met een Vector256 of Vector128 met twee overlappende laadbewerkingen — één vanaf het begin en één vanaf het einde — zodat de afhandeling van een breder restant voor de weggelaten Vector512/Vector256-codepaden rechtstreeks in de sprongtabel zit. De twee ladingen overlappen wanneer de lengte niet exact een veelvoud van de breedte is, dus de staart wordt gemaskeerd aan de additieve identiteit met ConditionalSelect voordat deze wordt opgeteld. Dat masker is alleen nodig omdat toevoeging niet-idempotent is; een idempotente bewerking zoals een zoekopdracht kan de overlappende staart rechtstreeks invouwen. Een buffer op hardware zonder vectorisatie valt helemaal door tot SumScalar, een gewone scalaire lus.

Doorloop de invoer en verwerk het restant

Als u een buffer wilt verwerken die groter is dan één vector, loopt u deze over een vector tegelijk en verwerkt u vervolgens de restelementen die geen volledige vector vullen. De robuuste manier om die staart af te handelen is om de elementen ter grootte van de laatste volledige vector opnieuw te verwerken, waarbij er overlap is met elementen die de lus al heeft verwerkt, waardoor een aparte scalaire epiloog wordt vermeden. Of die overlapping moet worden gecorrigeerd, is afhankelijk van de bewerking.

Een niet-idempotente bewerking, zoals een som, telt de overlappende elementen tweemaal, dus maskeer ze tot het identiteitselement van de bewerking voordat je ze erin meeneemt. Gebruik dit wanneer elk element precies één keer moet bijdragen:

// Sums a buffer with an unrolled vector loop plus a masked, jump-table remainder.
private static T SumVector128<T>(ReadOnlySpan<T> buffer)
    where T : unmanaged, INumberBase<T>
{
    Debug.Assert(Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported);
    Debug.Assert(buffer.Length >= Vector128<T>.Count);

    // Preload the last full vector, overlapping the tail. Any sub-vector remainder is folded in
    // from here (masked) by case 0 of the switch below, so the loop never falls out to a separate
    // scalar tail—the same shape TensorPrimitives uses.
    Vector128<T> end = Vector128.Create(buffer.Slice(buffer.Length - Vector128<T>.Count));

    // A production implementation would also align the buffer to a vector boundary and, for
    // very large inputs, use non-temporal loads/stores so the data doesn't evict useful
    // cache lines. Both are omitted here; see TensorPrimitives for a complete treatment.

    Vector128<T> sum = Vector128<T>.Zero;

    // Only pay for the four independent accumulators when there's enough data to unroll;
    // smaller payloads skip straight to the remainder below. Four vectors per iteration lets
    // the accumulators pipeline; Vector128.Create reads the first Vector128<T>.Count elements.
    if (buffer.Length >= Vector128<T>.Count * 4)
    {
        Vector128<T> sum0 = Vector128<T>.Zero;
        Vector128<T> sum1 = Vector128<T>.Zero;
        Vector128<T> sum2 = Vector128<T>.Zero;
        Vector128<T> sum3 = Vector128<T>.Zero;

        do
        {
            sum0 += Vector128.Create(buffer);
            sum1 += Vector128.Create(buffer.Slice(Vector128<T>.Count));
            sum2 += Vector128.Create(buffer.Slice(Vector128<T>.Count * 2));
            sum3 += Vector128.Create(buffer.Slice(Vector128<T>.Count * 3));

            buffer = buffer.Slice(Vector128<T>.Count * 4);
        }
        while (buffer.Length >= Vector128<T>.Count * 4);

        // Combine pairwise so the two independent adds can pipeline.
        sum = (sum0 + sum1) + (sum2 + sum3);
    }

    // Split the remainder into its full vectors and a sub-vector tail. The full vectors fall
    // through the jump table; the tail lands in case 0, where the preloaded end is masked so only
    // the trailing elements the full vectors didn't already cover are added.
    (int blocks, int trailing) = Math.DivRem(buffer.Length, Vector128<T>.Count);

    switch (blocks)
    {
        case 3:
        {
            sum += Vector128.Create(buffer.Slice(Vector128<T>.Count * 2));
            goto case 2;
        }

        case 2:
        {
            sum += Vector128.Create(buffer.Slice(Vector128<T>.Count));
            goto case 1;
        }

        case 1:
        {
            sum += Vector128.Create(buffer);
            goto case 0;
        }

        case 0:
        {
            Vector128<T> msk = CreateRemainderMask128<T>(trailing);
            sum += Vector128.ConditionalSelect(msk, end, Vector128<T>.Zero);
            break;
        }
    }

    // Horizontally add the lanes into a single scalar.
    return Vector128.Sum(sum);
}

Deze versie schermt de afgewikkelde lus af met een if, zodat kleine payloads de vier accumulators volledig overslaan en meteen doorgaan naar de restverwerking. Wanneer er voldoende gegevens zijn, verzamelt een do/while iteratie vier vectoren per iteratie in onafhankelijke accumulators, waarmee de processorpijplijn de toevoegingen kan uitvoeren en deze paarsgewijs combineert. Een switch jumptabel verwerkt vervolgens ook de resterende nul tot drie volledige vectoren en, in case 0, de rest van de subvector: die hergebruikt een volledige vector die vooraf aan het einde van de buffer is geladen, overlapt daarbij de elementen die al zijn verwerkt, en maskeert die overlap met ConditionalSelect tot de additieve identiteit, zodat de staart gevectoriseerd blijft in plaats van terug te vallen op een scalaire lus. Net als voorheen leest Vector128.Create elementen van Vector128<T>.Count uit de span. De JIT slaat de grenscontrole van de span over bij typische toegangspatronen, dus Create is een prima standaardkeuze, zelfs in een intensieve lus; LoadUnsafe (hieronder besproken) is het alternatief op een lager niveau voor wanneer u de buffer via een beheerde verwijzing doorloopt. Voor zeer grote invoer zou een volledige implementatie ook de buffer uitlijnen en niet-temporele loads en stores gebruiken om te voorkomen dat nuttige cachelijnen uit de cache worden verdrongen—beide zijn hier weggelaten en worden volledig behandeld door TensorPrimitives.

Een idempotente bewerking, zoals het zoeken naar een waarde, kan de overlap ongevaarlijk opnieuw verwerken, zodat de laatste vector direct zonder masker wordt gevouwen:

// Idempotent search that re-processes the final vector instead of a scalar loop.
public static bool Contains(ReadOnlySpan<int> buffer, int searched)
{
    Debug.Assert(Vector128.IsHardwareAccelerated);

    Vector128<int> values = Vector128.Create(searched);
    ReadOnlySpan<int> remaining = buffer;

    while (remaining.Length >= Vector128<int>.Count)
    {
        if (Vector128.EqualsAny(Vector128.Create(remaining), values))
        {
            return true;
        }
        remaining = remaining.Slice(Vector128<int>.Count);
    }

    if (remaining.IsEmpty)
    {
        return false;
    }

    // A partial vector remains. When the buffer holds at least one full vector,
    // re-check the last one (overlapping the tail); otherwise scan the few elements directly.
    if (buffer.Length >= Vector128<int>.Count)
    {
        Vector128<int> tail = Vector128.Create(buffer.Slice(buffer.Length - Vector128<int>.Count));
        return Vector128.EqualsAny(tail, values);
    }

    foreach (int value in remaining)
    {
        if (value == searched)
        {
            return true;
        }
    }

    return false;
}

Warning

Onjuiste verwerking van de rest is een veelvoorkomende bron van bugs. Een lus die voorbij het einde van de buffer leest, produceert niet-deterministische resultaten en kan vastlopen. De testsuite van de runtime maakt gebruik van een BoundedMemory-hulpfunctie die direct na de buffer een ontoegankelijke pagina plaatst, zodat elke leesbewerking buiten de buffergrenzen tijdens het testen een AccessViolationException veroorzaakt. Bedek altijd de restlogica, inclusief buffers waarvan de lengte geen veelvoud is van de vectorbreedte.

Vectoren veilig laden en opslaan

Voor de meeste code Vector128.Create(span) en CopyTo zijn de eenvoudigste manier om gegevens te verplaatsen tussen een span en een vector, en de JIT houdt ze efficiënt. Wanneer u lees- en schrijfbewerkingen op een lager niveau nodig hebt, bijvoorbeeld om met een beheerde verwijzing door een buffer te lopen, gebruikt u bij voorkeur de overloads LoadUnsafe en StoreUnsafe, die een beheerde verwijzing en een nuint elementoffset accepteren. In tegenstelling tot de op pointers gebaseerde Load/Store-overbelastingen hoeven ze de buffer niet vast te zetten, en in tegenstelling tot ruwe referentiearitmetiek hoeft u een ref niet handmatig te verhogen. Beide alternatieven kunnen gemakkelijk verkeerd uitpakken op manieren die gaten in de garbagecollection of toegangsschendingen veroorzaken.

Zodat lege buffers niet worden gegooid, haalt u de beginreferentie op uit GetReference (of GetArrayDataReference voor matrices) in plaats ref span[0]van .

Important

Offsetberekeningen maken gebruik van niet-ondertekende nuint. Controleer altijd de bufferlengte voordat u een offset zoals buffer.Length - Vector128<int>.Count berekent. Als de buffer kleiner is dan één vector, onderloopt die aftrekking naar een enorme waarde en leest de lus uit ongeldig geheugen.

Platformspecifieke hardware-intrinsieken

Wanneer een specifieke processorinstructie u een voordeel biedt dat de draagbare API's niet ontsluiten, grijp dan naar de hardware-intrinsics in System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm en System.Runtime.Intrinsics.Wasm. Elke intrinsieke klasse heeft een IsSupported eigenschap (ook een JIT-constante), zodat u het gespecialiseerde pad kunt bewaken en ergens anders kunt terugvallen op draagbare code:

// Illustrates per-platform lightup. The portable '(vector & mask) == Zero' below
// already lowers optimally, so prefer it unless a specific instruction measurably wins.
public static bool AllBitsClear(Vector128<byte> vector, Vector128<byte> mask)
{
    if (Sse41.IsSupported)
    {
        // x86/x64: a single ptest instruction.
        return Sse41.TestZ(vector, mask);
    }
    else if (AdvSimd.Arm64.IsSupported)
    {
        // Arm64: AND, then reduce the maximum byte across every lane.
        Vector128<byte> anded = AdvSimd.And(vector, mask);
        return AdvSimd.Arm64.MaxAcross(anded).ToScalar() == 0;
    }
    else if (PackedSimd.IsSupported)
    {
        // WebAssembly: AND, then test whether any lane is non-zero.
        return !PackedSimd.AnyTrue(PackedSimd.And(vector, mask));
    }
    else
    {
        // Portable fallback for any other platform.
        return (vector & mask) == Vector128<byte>.Zero;
    }
}

De voorgaande methode laat zien hoe u codepaden per architectuur kunt oplichten wanneer u ze wilt, maar het is bewust een eenvoudig voorbeeld: u hebt deze hier niet echt nodig. De draagbare expressie (vector & mask) == Vector128<byte>.Zero verlaagt al tot de optimale instructie op elk platform (bijvoorbeeld ptest op x86/x64), dus het doet hetzelfde werk als de handgeschreven vertakkingen, alleen zonder de complexiteit. Grijp alleen naar expliciete intrinsics wanneer een specifieke instructie meetbaar beter presteert dan wat de portable API's genereren.

Hardware-intrinsics vereisen voor elke instructieset een afzonderlijke implementatie, dus beschouw ze als een optimalisatie voor kritieke codepaden die uit metingen blijken, en niet als standaard. De Vector128/Vector256 API's zijn al lager dan efficiënte instructies op elk platform en in de praktijk wint geavanceerde code per instructie niet altijd. Bevestig het verschil met een benchmark voordat u het extra onderhoud doorvoert.

Wiskunde op een hoger niveau met TensorPrimitives

Als u gevectoriseerde wiskunde voor spans nodig hebt en de lussen niet zelf wilt schrijven, biedt TensorPrimitives een groot aantal numerieke bewerkingen, zoals elementsgewijze rekenkundige bewerkingen, exponentiële functies en reducties zoals dotproduct en cosinussimilariteit, die intern al gevectoriseerd zijn. Het is beschikbaar in het NuGet-pakket System.Numerics.Tensors .

// Computes result = (left * right) + addend over the whole span, vectorized internally.
public static float[] MultiplyAdd(float[] left, float[] right, float[] addend)
{
    float[] result = new float[left.Length];

    TensorPrimitives.Multiply(left, right, result);
    TensorPrimitives.Add(result, addend, result);

    return result;
}

// Higher-level reductions are available too.
public static float CosineSimilarity(float[] left, float[] right) =>
    TensorPrimitives.CosineSimilarity(left, right);

Voor AI- en numerieke workloads levert TensorPrimitives vaak het grootste deel van de voordelen van handgeschreven SIMD, zonder de complexiteit.

Alle codepaden testen

Omdat een vectorized methode verschillende codepaden heeft, moeten tests elk pad omvatten: het Vector256 pad, het Vector128 pad en het scalaire pad, elk met invoer zowel groot genoeg als te klein om te profiteren. U kunt de invoergrootte in tests variëren, maar u kunt hardwareversnelling niet in-/uitschakelen op testniveau. Beheer het in plaats daarvan met omgevingsvariabelen voordat het proces wordt gestart:

  • Stel DOTNET_EnableAVX2=0 in zodat Vector256.IsHardwareAcceleratedfalse retourneert.
  • Stel DOTNET_EnableHWIntrinsic=0 in om intrinsics volledig uit te schakelen, zodat Vector128, Vector64 en Vector<T> allemaal geen acceleratie melden.

Om alle paden op één machine te doorlopen, voert u de testsuite eenmaal uit zonder overschrijvingen, eenmaal met DOTNET_EnableAVX2=0 en eenmaal met DOTNET_EnableHWIntrinsic=0. Het alternatief is om het op voldoende uiteenlopende hardware te draaien om die gevallen af te dekken.

Configuratieopties voor de instructieset

Naast deze twee herkent de runtime een knop per logische groepering van instructiesets, elk met voorvoegsel.DOTNET_ Eén knop kan verschillende gerelateerde instructiesets dekken,EnableAVX2 bijvoorbeeld poorten AVX2, samen met BMI1, BMI2, F16C, FMA, LZCNT en MOVBE. Een knop instellen op 0 schakelt de hele groep uit en alles wat daarboven ligt. Door deze op 1 in te stellen (de standaardinstelling voor de meeste gebruikers) wordt de groep toegestaan, maar de hardware moet die dan nog wel daadwerkelijk ondersteunen—het inschakelen van een optie die de huidige CPU niet ondersteunt, wordt genegeerd, dus u kunt alleen beperken wat er wordt gebruikt, en nooit een niet-ondersteunde instructie afdwingen en zo problemen veroorzaken. DOTNET_EnableHWIntrinsic=0 is de botte bijl: het schakelt alles uit tot op het basisniveau, zodat Vector128, Vector64 en Vector<T> allemaal aangeven dat er geen acceleratie is en de code terugvalt op het softwarepad.

Important

Dit zijn diagnostische hulpprogramma's, die voornamelijk bedoeld zijn voor testen en valideren, waarbij elk codepad wordt uitgevoerd, een hardwarespecifiek probleem wordt gereproduceert of een terugval wordt bevestigd. Ze zijn niet ontworpen voor algemeen of productiegebruik en zijn geen stabiliteitscontract. De volgende set is wat .NET 11 herkent; eerdere releases hebben een andere set weergegeven: de basislijn- en AVX-512-knoppen in het bijzonder zijn opnieuw geconfigureerd, dus bevestig de namen ten opzichte van de runtimeversie waarop u zich richt.

Deze hulpprogramma's hebben ook limieten voor wat ze bereiken. Omdat ze JIT-beslissingen gateen, hebben ze geen invloed op code die al van tevoren is gecompileerd via ReadyToRun of Native AOT, en ze hebben niet noodzakelijkerwijs invloed op interne routines die de runtime- en kernbibliotheken zelf gebruiken. Behandel ze als een manier om uw eigen JIT-gecompileerde code te sturen, niet als een globale uitschakeloptie voor een instructieset.

De basisschakelaar en de breedtelimieten zijn van toepassing op elke architectuur:

Knop (voorvoegsel DOTNET_) Default Effect
EnableHWIntrinsic 1 Hoofdswitch voor alle hardware-intrinsieken; 0 dwingt het volledige softwarepad af.
MaxVectorTBitWidth systeemstandaard Beperkt Vector<T> tot een maximale breedte in bits; een waarde lager dan 128 betekent de systeemstandaard.
PreferredVectorBitWidth systeemstandaard Begrenst de maximale vector met vaste lengte die wordt gerapporteerd als IsHardwareAccelerated, in bits; een waarde onder 128 betekent de standaardwaarde van het systeem.

De systeemstandaard voor MaxVectorTBitWidth kan beperkter zijn dan wat de hardware volledig ondersteunt, dus Vector<T> wordt niet automatisch vergroot tot de breedste beschikbare vector. Bijvoorbeeld, Vector512<T>.IsHardwareAccelerated kan true zijn terwijl Vector<T> 256-bit blijft; stel DOTNET_MaxVectorTBitWidth=512 in om Vector<T> de grotere breedte te laten gebruiken.

PreferredVectorBitWidth beperkt de maximale vectorbreedte die IsHardwareAccelerated rapporteert. Als u deze verlaagt tot onder wat de hardware ondersteunt, schakelt dat de bredere vectorbreedtes uit: op een machine die 512-bits vectoren ondersteunt, zorgt DOTNET_PreferredVectorBitWidth=256 ervoor dat Vector512<T>.IsHardwareAcceleratedfalse rapporteert. Het is een algemene knop, maar alleen x86/x64 biedt breedtes boven 128 vandaag, dus dat is de enige plaats die het een waarneembaar effect heeft.

Elke logische groepering van x86/x64 instructiesets heeft ook een eigen switch:

Knop (voorvoegsel DOTNET_) Default Gates
EnableAVX 1 AVX en afhankelijke personen
EnableAVX2 1 AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE en afhankelijken
EnableAVX512 1 AVX-512 F+BW+CD+DQ+VL en afhankelijkheden
EnableAVX512BMM 1 AVX-512 BMM
EnableAVX512v2 1 AVX-512 IFMA+VBMI
EnableAVX512v3 1 AVX-512 BITALG+VBMI2+VPOPCNTDQ+VNNI
EnableAVX10v1 1 AVX10.1
EnableAVX10v2 0 AVX10.2
EnableAPX 0 APX (uitgebreide registers voor algemeen gebruik)
EnableAES 1 AES, PCLMULQDQ
EnableAVX512VP2INTERSECT 1 AVX-512 VP2INTERSECT
EnableAVXIFMA 1 AVX-IFMA
EnableAVXVNNI 1 AVX-VNNI
EnableAVXVNNIINT 1 VEX AVX-VNNI-INT8 en AVX-VNNI-INT16
EnableGFNI 1 GFNI
EnableSHA 1 SHA
EnableVAES 1 VAES, VPCLMULQDQ
EnableWAITPKG 1 WAITPKG
EnableX86Serialize 1 X86 SERIALISEREN

Op Arm64 heeft elke logische groepering van instructiesets een eigen switch:

Knop (DOTNET_ voorvoegsel) Default Gates
EnableArm64Aes 1 AES
EnableArm64Atomics 1 Large System Extensions (LSE)-atomaire bewerkingen
EnableArm64Crc32 1 CRC32
EnableArm64Dczva 1 DC ZVA cache op nul zetten
EnableArm64Dp 1 Dot Product
EnableArm64Rdm 1 Afrondend verdubbelend vermenigvuldigen en accumuleren (RDM)
EnableArm64Sha1 1 SHA1
EnableArm64Sha256 1 SHA256
EnableArm64Rcpc 1 Releaseconsistente, processorconsistente volgorde (RCpc)
EnableArm64Rcpc2 1 RCpc2
EnableArm64Cssc 0 Common Short Sequence Compression (CSSC)
EnableArm64Sve 1 Scalable Vector Extension (SVE)
EnableArm64Sve2 1 SVE2
EnableArm64Sha3 1 SHA3
EnableArm64Sm4 1 SM4
EnableArm64SveAes 1 SVE AES
EnableArm64SveSha3 1 SVE SHA3
EnableArm64SveSm4 1 SVE SM4

Een instelling die standaard op 0 staat (bijvoorbeeld EnableAVX10v2 of EnableArm64Cssc) schakelt een instructieset in die nog beschikbaar komt, en blijft daarom uitgeschakeld totdat u ervoor kiest deze in te schakelen.

Benchmark om de overwinning te bevestigen

Vectorisatie voegt complexiteit toe, dus ga na of het loont voordat u ermee doorgaat. Gebruik BenchmarkDotNet en gebruik dezelfde omgevingsvariabelen die eerder werden weergegeven om de scalaire, Vector128en Vector256 implementaties in één uitvoering te vergelijken. De disassembly-diagnoser van BenchmarkDotNet kan ook de gegenereerde assemblycode weergeven, wat van onschatbare waarde is bij het optimaliseren van high-performancecode.

Een paar dingen om rekening mee te houden:

  • Grotere invoer heeft meer voordeel. Voor kleine buffers kan vectorized code langzamer zijn dan scalaire code vanwege de overhead van de installatie. Benchmark de invoergrootten die uw bellers daadwerkelijk gebruiken.
  • Versnellingen zijn zelden perfect. Een 256-bits vector die op 32-bits elementen werkt, zal niet per se 8x sneller zijn; geheugenbandbreedte, uitlijning en instructielatentie spelen allemaal een rol.
  • Geheugenuitlijning is van invloed op stabiliteit. Gerandomiseerde toewijzingsafstemming voegt ruis toe tussen uitvoeringen. U kunt uitgelijnd geheugen toewijzen met AlignedAlloc voor stabiele resultaten, of BenchmarkDotNet's geheugenrandomisatie inschakelen om de volledige verdeling te bekijken.

Beste praktijken

  • Grijp eerst naar de bestaande API’s op een hoger niveau. Span<T>, string, LINQ, TensorPrimitives en de tensortypen versnellen al veel algemene bewerkingen voor je—bouw niet zelf iets wat al geoptimaliseerd en getest is.
  • Start met Vector128<T>; het wordt versneld op de grootste verscheidenheid aan hardware, en u hebt Vector256<T> niet nodig om een correcte, draagbare implementatie te krijgen. Voeg alleen bredere breedten en hardware-intrinsiek toe voor gemeten dynamische paden.
  • Controleer IsHardwareAccelerated en Count rechtstreeks in plaats van ze in de cache op te slaan. De JIT verandert ze in constanten.
  • Houd altijd rekening met de rest van de lus en houd rekening met overlappende bron- en doelbuffers bij het opslaan.
  • Schrijf eerst edge-casetests, vervolgens een scalaire oplossing, en druk die scalaire logica vervolgens uit met de vector-API's.
  • Test elk codepad (inclusief toegangsschendingen) en voer benchmarks uit met realistische invoergrootten voordat u kiest voor die extra complexiteit.

Zie ook