Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
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:
- Vector2, Vector3en Vector4 vertegenwoordigt vectoren van 2, 3 en 4 Single waarden.
- Matrix3x2 en Matrix4x4 vertegenwoordigt 3x2 en 4x4 matrices van Single waarden.
- Plane vertegenwoordigt een vlak in driedimensionale ruimte.
- Quaternion vertegenwoordigt een vector die wordt gebruikt om driedimensionale rotaties te coderen.
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:
-
Vector.IsHardwareAccelerated geeft aan of
Vector<T>bewerkingen zijn versneld. -
Vector128.IsHardwareAccelerated, en de equivalenten
Vector64/Vector256/Vector512, rapporteren de versnelling voor elke vaste breedte.
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=0in zodat Vector256.IsHardwareAcceleratedfalseretourneert. - Stel
DOTNET_EnableHWIntrinsic=0in om intrinsics volledig uit te schakelen, zodatVector128,Vector64enVector<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,TensorPrimitivesen 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 hebtVector256<T>niet nodig om een correcte, draagbare implementatie te krijgen. Voeg alleen bredere breedten en hardware-intrinsiek toe voor gemeten dynamische paden. - Controleer
IsHardwareAcceleratedenCountrechtstreeks 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.