Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
SIMD (enkel instruktion, flera data) är maskinvarustöd för att tillämpa en åtgärd på flera datadelar parallellt med en enda instruktion. Vektoriserad kod bearbetar flera värden per iteration i stället för ett, vilket avsevärt kan öka dataflödet för den typ av numeriskt, vetenskapligt, grafik, textbearbetning och dataparallellt arbete där samma åtgärd upprepas över en buffert. Nackdelen är ökad komplexitet, så det lönar sig främst när indatamängden är tillräckligt stor och vinsten har bekräftats genom mätningar.
.NET har flera typer av SIMD-stöd. Välj den som matchar hur mycket kontroll du behöver och hur mycket komplexitet du är villig att ta på dig.
Typer av SIMD-stöd i .NET
| API (applikationsprogrammeringsgränssnitt) | Namespace | När du ska använda detta |
|---|---|---|
| Typer av vektorer och matriser med fast användning | System.Numerics | Grafik- och geometrimatematik med 2–4 elementvektorer, matriser, quaternions och plan. |
Vector<T> |
System.Numerics | Portabel vektorisering med variabel bredd när du inte behöver kontroll per plattform. |
Vector64<T>, Vector128<T>, , Vector256<T>Vector512<T> |
System.Runtime.Intrinsics | Plattformsoberoende vektorisering med fast bredd med detaljerad kontroll. Detta är den rekommenderade startpunkten för nya vektoriserade algoritmer. |
| Inbyggd maskinvara | System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm, System.Runtime.Intrinsics.Wasm | Specifika processorinstruktioner som API:erna på högre nivå inte exponerar för den sista prestandan på en frekvent sökväg. |
TensorPrimitives |
System.Numerics.Tensors | Färdig, vektoriserad matematik över intervall. Det gör vektoriseringen åt dig. |
Om dessa API:er överlappar varandra relaterar de via abstraktionslager. De generiska vektortyperna är de grundläggande utbytestyperna som de andra lagren passerar runt, så de är tekniskt sett den lägsta nivån: variabelbredden Vector<T>, som växer till den bredd som den löpande maskinvaran stöder och den fasta bredden Vector64<T> genom Vector512<T>. De plattformsspecifika maskinvaruintrinsikerna i System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm och System.Runtime.Intrinsics.Wasm arbetar på dessa typer, och var och en mappas direkt till en enskild processorinstruktion. De plattformsoberoende operationerna som exponeras för de generiska typerna ligger ett steg ovanför de plattformsspecifika intrinsiska funktionerna och översätts till dem för respektive målplattform. Högre är fortfarande de hanterade API:er som fungerar över hela buffertar – vektoriserade metoder på Span<T> och string, och TensorPrimitives– som bygger på lagren nedan, så att du får SIMD-acceleration utan att skriva någon av dem för hand. Typerna System.Numerics med fast form är domänspecifika bekvämlighetstyper för grafik och geometri i stället för en del av den här utbytesstacken.
Resten av den här artikeln går igenom dessa API:er från den högsta nivån till den lägsta och omfattar sedan testning, benchmarking och bästa praxis.
System.Numerics-vektor- och matristyper
Namnområdet System.Numerics innehåller SIMD-accelererade typer med en fast form:
- Vector2, Vector3och Vector4 representerar vektorer med värden 2, 3 och 4 Single .
- Matrix3x2 och Matrix4x4 representerar 3x2- och 4x4-matriser med Single värden.
- Plane representerar ett plan i tredimensionellt utrymme.
- Quaternion representerar en vektor som används för att koda tredimensionella rotationer.
Dessa typer passar naturligt för grafik och geometri, och körningsmiljön accelererar deras operationer med SIMD-instruktioner där maskinvaran stöder det. I följande exempel läggs två vektorer till:
Vector2 v1 = Vector2.Create(0.1f, 0.2f);
Vector2 v2 = Vector2.Create(1.1f, 2.2f);
Vector2 sum = v1 + v2;
De exponerar också den vanliga vektormatematik som du kan förvänta dig, till exempel punktprodukt, avstånd och klämning:
float dot = Vector2.Dot(v1, v2);
float distance = Vector2.Distance(v1, v2);
Vector2 clamped = Vector2.Clamp(v1, Vector2.Zero, Vector2.One);
Matristyperna stöder matrismatematik, till exempel transponering och multiplikation:
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);
Vektor<T>
Vector<T> representerar en variabelbreddsvektor av en primitiv numerisk typ. Dess längd är fast under processens livslängd, men värdet Vector<T>.Count för beror på vilken PROCESSOR som kör koden. Kompilatorn Just-In-Time (JIT) behandlar Count som en konstant, så loopar som skrivs mot den optimerar bra.
Vector<T> ger dig bärbar vektorisering utan per plattformskod, på bekostnad av att inte känna till vektorbredden vid kompileringstiden. I följande exempel beräknas elementmässigt tillägg av två matriser:
// 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;
}
Anmärkning
Det här exemplet är illustrativt. Du behöver sällan skriva en loop som den här för hand, eftersom TensorPrimitives redan erbjuder accelererad, span-baserad matematik. Den här elementvisa additionen är Add, och reduktionsoperationer som Sum finns också. Dessa åtgärder är maskinvaruaccelererade för de elementtyper som Vector<T> stöder (Vector<T>.IsSupported).
Kontrollera maskinvaruacceleration
SIMD-accelererade typer fungerar även på maskinvaru- eller JIT-konfigurationer som inte stöder SIMD, eftersom de återgår till icke-accelererade programvaruimplementeringar. Om du vill avgöra om acceleration verkligen är tillgänglig kontrollerar du den relevanta egenskapen IsHardwareAccelerated:
-
Vector.IsHardwareAccelerated rapporterar om
Vector<T>åtgärder påskyndas. -
Vector128.IsHardwareAccelerated, och motsvarande
Vector64/Vector256/Vector512rapporterar acceleration för varje fast bredd.
Dessa egenskaper omvandlas till konstanter av JIT, så de grenar du inte tar elimineras och det finns ingen körningskostnad för att kontrollera dem. Lagra inte värdena i cache; läs dem direkt där du behöver dem. Detsamma gäller för egenskaperna Count (till exempel Vector128<T>.Count), som också är konstanter vid JIT-kompilering.
De flesta åtgärder med en accelererad bredd accelereras själva, men det är inte garanterat för varje åtgärd. Till exempel kan flyttalsdivisionen accelereras där heltalsdivisionen inte är det. När Vector256 accelereras gör Vector128 det vanligtvis också, men det finns ingen garanti, så kontrollera varje bredd du använder.
Tip
Om en åtgärd som du behöver inte påskyndas på en plattform som du bryr dig om, eller om du vill ha ett nytt plattformsoberoende API, kan du skapa ett problem med dotnet/runtime. Samma sak gäller för förbättringar av codegen.
Alla elementtyper är inte giltiga för varje vektor.
Vector128<T> och dess syskon stöder de primitiva numeriska typerna (byte, sbyte, short, ushort, int, uint, long, ulong, float, double, nint och nuint) i dag, och den uppsättningen kan växa till att omfatta andra typer i framtiden. Använd Vector128<T>.IsSupported för att avgöra om en given T är giltig, vilket är särskilt användbart från allmän kod.
Typer som inte stöds, till exempel char och bool, kan fortfarande vektoriseras genom att omtolka bufferten som en typ av samma storlek som stöds. Använd Cast för att omtolka ett spann, char till exempel till ushort, eller vektorns As<TFrom, TTo> metod för att omtolka en vektor som du redan har. Omtolkningen ändrar bara typen, inte de underliggande bitarna, så det är ditt ansvar att hålla data välformulerad: ett bool måste stanna 0 eller 1, och ett char måste förbli en giltig UTF-16-kodenhet. Om en vektoriserad åtgärd kan generera ett värde som ligger utom intervallet bör du normalisera resultatet innan du skriver tillbaka det.
Plattformsoberoende vektorisering med Vector128
Vector128<T> är den gemensamma nämnaren för varje plattform som stöder vektorisering, så det är det bästa stället att börja på. Den har en 128-bitars vektor: 16 byte, 8 shorts, 4 ints/floats eller 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> är dubbelt så bred, och Vector512<T> dubbelt så bred igen. Inte all maskinvara har stöd för de större bredderna, så exemplen som följer används Vector128 för portabilitet.
Varje bredd har en allmän typ (Vector128<T>) för data och en icke-generisk statisk klass (Vector128) som innehåller de flesta åtgärder, inklusive statiska fabriksmetoder som Create och Load. Operatorer som +, &och << är det idiomatiska sättet att uttrycka aritmetiska åtgärder och bitåtgärder. Föredra dem framför de namngivna metodekvivalenterna för att undvika operatorprioretiska buggar och förbättra läsbarheten. För algoritmer som bygger på byteordning gör du en villkorssats baserat på IsLittleEndian, som JIT-kompilatorn också reducerar till en konstant.
Anmärkning
På x86/x64 behandlas Vector256<T> operationer vanligtvis som två oberoende 128-bitars "banor". För de flesta elementvisa operationer märks detta inte, men operationer som korsar banor (till exempel omordningar eller parvisa/horisontella operationer) kan fungera annorlunda eller kosta mer än motsvarande Vector128. Bekräfta med benchmarks innan du antar att en bredare vektor är snabbare.
Lane-crossing-åtgärder breddas inte gratis
Elementbaserade åtgärder bryr sig inte om bredd: v1 + v2 är samma resultat per element oavsett om v1 och v2 är Vector128<T> eller Vector256<T>– breddning bearbetar bara ett körfälts värde för data per instruktion.
Add på v = [a, b, c, d] och w = [e, f, g, h] kombinerar alltid samma indexelement:
v: [ a | b | c | d ]
w: [ e | f | g | h ]
+ + + +
r: [a+e|b+f|c+g|d+h]
Filkorsningsåtgärder skalas inte upp helt enkelt, eftersom vilka element som kombineras beror på vektorns bredd. En parvis reduktion kombinerar intilliggande element i stället för element med samma index, så ändrar widening vilka element som till slut paras ihop:
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)
Det är precis vad en horisontell reduktion gör: summerar elementen i en vektor med två omgångar av parvisa additioner. På x86/x64 Vector128<float> tar (4 element) dig dit med två anrop till 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();
}
Utöka samma mönster med två anrop till Vector256<float> (8 element) så ser det rätt ut – men det är det inte.
HorizontalAdd arbetar inte över hela 256-bitarsvektorn; det parvisa mönstret upprepas oberoende inom varje 128-bitarshalva. Två omgångar ger dig summan för den nedre halvan (element 0–3) replikerad över den nedre halvan och summan för den övre halvan (element 4–7) replikerad över den övre halvan, inte summan av alla åtta 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();
}
För att få rätt totalsumma måste du uttryckligen överbrygga körfältsgränsen: läs varje körfälts delsumma med GetLower/GetUpper och lägg ihop dem—GetLower och GetUpper delar upp en vektor i dess första och andra halvor:
------------------------------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();
}
Det extra steget är den verkliga kostnaden för att korsa körfält. En algoritm för korsning av körfält vidgas inte bara kostnadsfritt på samma sätt som en elementmässigt gör – mät innan du antar att den bredare vektorn vinner.
Vanliga åtgärder
Vector128 och dess bredare syskon exponerar en stor API-yta. Du behöver inte memorera det – känna till kategorierna och leta upp informationen när du behöver dem. Varje åtgärd har en programåterställning för plattformar som inte kan påskynda den. Följande tabell täcker i stort sett hela ytan.
| Category | Vad det gör | Exempel-API:er |
|---|---|---|
| Constants | Fördefinierade konstantvektorer |
Zero, One, NegativeOne, AllBitsSet, Indices, SignSequence, E, Pi, Tau, Epsilon, NaN, PositiveInfinity, NegativeInfinity, NegativeZero |
| Skapandet | Sända en skalär, ange element eller generera en sekvens |
Create, CreateScalar, CreateScalarUnsafe, , Create(ReadOnlySpan<T>), CreateSequence, CreateGeometricSequence, , CreateHarmonicSequence, CreateAlternatingSequence |
| Läsa in och lagra | Flytta data mellan minne och en vektor |
Load, LoadUnsafe, LoadAligned, LoadAlignedNonTemporal, Store, StoreUnsafe, , StoreAligned, StoreAlignedNonTemporal, , CopyToTryCopyTo |
| Arithmetic | Elementvis matematik och reduktioner |
Add (x + y), Subtract (x - y), Multiply (x * y), Divide (x / y), Negate (-x), AddSaturate, SubtractSaturate, Abs, Sqrt, FusedMultiplyAdd, Dot, Sum |
| Bitåtgärder | Bitvis logik och skiftoperationer |
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 och kläm | Elementvis minimum, maximum och begränsning till ett intervall |
Min, Max, Clamp, MinMagnitude, MaxMagnitude, MinNumber, MaxNumberMinMagnitudeNumberMaxMagnitudeNumber |
| Avrundning | Avrunda varje element till ett integralvärde |
Ceiling, Floor, , RoundTruncate |
| Matematiska funktioner | Hjälpfunktioner för tecken, interpolation, vinklar och transcendentala funktioner |
CopySign, Lerp, DegreesToRadians, RadiansToDegrees, Hypot, Sin, Cos, SinCos, Asin, Exp, Log, Log2 |
| Jämförelse | Jämför varje element; resultatet är en vektormask, inte den som operatorn bool ger |
Equals, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual |
| Classification | Predikat per element över tallinjen, som vart och ett returnerar en vektormask |
IsNaN, IsFinite, IsInfinity, IsPositiveInfinity, IsNegativeInfinity, IsInteger, IsEvenInteger, IsOddInteger, IsNegative, IsPositive, IsNormal, IsSubnormal, IsZero |
| Jämförelseminskningar | Fäll ihop en jämförelse på elementnivå till ett enda bool |
EqualsAll (x == y), EqualsAny, GreaterThanAll, GreaterThanAny, GreaterThanOrEqualAll, GreaterThanOrEqualAny, , LessThanAll, LessThanAny, , LessThanOrEqualAllLessThanOrEqualAny |
| Predikat för hela vektorer | Reducera en vektor till ett bool: om alla, något eller inga element är lika med ett värde, eller om (de WhereAllBitsSet-varianterna) har alla bitar satta. Föredra dessa framför att konvertera en mask till ett index |
All, Any, None, AllWhereAllBitsSet, AnyWhereAllBitsSet, NoneWhereAllBitsSet |
| Search | Räkna eller hitta element utifrån värde, eller (varianterna WhereAllBitsSet) ställ in banor i en mask |
Count, IndexOf, LastIndexOf, CountWhereAllBitsSet, IndexOfWhereAllBitsSet, LastIndexOfWhereAllBitsSet |
| Mask för index | Omvandla en jämförelsemask till en skalär bitmask och skanna den |
ExtractMostSignificantBits med TrailingZeroCount eller LeadingZeroCount |
| Urval | Blanda två vektorer enligt en mask, bit för bit |
ConditionalSelect(x, y, z), motsvarande (y & x) \| (z & ~x) |
| Conversion | Ändra numerisk typ, beräkna nya värden (till exempel int till float) |
ConvertToInt32, ConvertToInt64, ConvertToUInt32, ConvertToUInt64, ConvertToSingle, ConvertToDouble |
| Breddning och förträngning | Dela upp element till en bredare typ eller packa dem till en smalare typ |
Widen, WidenLower, WidenUpper, Narrow, NarrowWithSaturation |
| Omtolkning | Omtolka bitarna som en annan elementtyp utan att ändra dem |
As<TFrom, TTo>, AsByte, AsInt32, AsSingleoch de andra As* elementformulären |
| System.Numerics interop | Omtolka mellan Vector128<T> och de numeriska typerna med fast form |
AsVector, AsVector2, AsVector3, , AsVector4, AsPlane, AsQuaternion, , AsVector128, AsVector128Unsafe |
| Omisera | Ordna om element efter index eller mellanlagrar två vektorer |
Shuffle, Reverse, Zip, ZipLower, ZipUpper, Unzip, UnzipEven, UnzipOdd, ConcatLowerLower, ConcatLowerUpper, ConcatUpperLower, ConcatUpperUpper |
| Åtkomst till körfält | Läsa eller ersätta enskilda element och halvor eller ändra storlek på vektorn |
GetElement, WithElement, ToScalar, , GetLower, GetUpper, WithLower, , WithUpper, ToVector256 |
Tip
Flera åtgärder har Estimate- och Native-varianter, som till exempel MultiplyAddEstimate, ClampNative, MinNative, MaxNative, ShuffleNative och ConvertToInt32Native. De motsvarar en snabbare maskinvaruinstruktion som byter bort en del precision eller avstår från en IEEE-garanti för specialfall (till exempel NaN-hantering), så använd dem bara när ett prestandatest visar att den exakta varianten är flaskhalsen och den friare semantiken är acceptabel.
Anmärkning
Vector256.Shuffle behandlar sina indata som en enda 256-bitarsvektor, där plattformsspecifika Avx2.Shuffle fungerar som två oberoende 128-bitars körfält. Plattformsoberoende API är det mer portabla valet, men bekräfta det beteende du behöver när du porterar handskrivna inbyggda objekt.
Strukturera kodvägen
En vektoriserad metod förgrenar sig vanligtvis i en kodväg per vektorbredd samt en skalär reservväg för små indata och maskinvara utan acceleration. Om du vill använda den största vektor som maskinvaran stöder kontrollerar du först den bredaste vektorn och arbetar nedåt:
// 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);
}
Det yttre skyddet för varje bredd kombinerar Vector128.IsHardwareAccelerated (en konstant vid JIT-kompilering som anger om plattformen accelererar den bredden) med Vector128<T>.IsSupported (om elementtypen T är giltig för den bredden). I ett block som stöds jämför du indatalängden med Count för att välja mellan den vektoriserade kodvägen och en reservväg för små indata. Metoden är allmän över T, och blocken Vector256 och Vector512 – identiska med Vector128 blocket men med den bredare typen – visas kommenterade för korthet.
Det finns två separata reservlösningar. En buffert som är för liten för även den smalaste vektorn, men på accelererad maskinvara, går till SumVectorSmall– en explicit switch jump-tabell som hanterar varje möjlig delvektorlängd utan en loop:
// 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) är också en konstant vid JIT-kompilering, så SumVectorSmall väljer utifrån elementbredden en tabell dimensionerad för den bredaste vektorns antal element – samma metod som TensorPrimitives använder. (När den nya minnessäkerhetsmodellen är aktiverad sizeof(T) tillåts uttryck för en typparameter med villkoret unmanaged i säker kod.) Endast tabellen med 4 byte visas. tabellerna 1 byte, 2 byte och 8 byte delar dess form. Dess större fall viker överblivna med en Vector256 eller Vector128 med två överlappande belastningar – en från början, en från slutet – så den bredare resthanteringen för de utelämnadeVector512/Vector256sökvägarna finns direkt i hopptabellen. De två belastningarna överlappar när längden inte är en exakt multipel av bredden, så svansen maskeras till den additiva identiteten med ConditionalSelect innan den summeras. Masken behövs bara eftersom addition inte är idempotent; en idempotent operation, till exempel en sökning, kan direkt inkludera den överlappande slutdelen. En buffert på hårdvara utan någon vektorisering går vidare till SumScalar, en vanlig skalär slinga.
Loopa över indata och hantera resten
Om du vill bearbeta en buffert som är större än en enskild vektor loopar du över en vektor i taget och hanterar sedan de överblivna elementen som inte fyller en fullständig vektor. Det robusta sättet att hantera slutet är att bearbeta om den sista omgången med en hel vektors element, så att vissa element som loopen redan har hanterat överlappar, vilket gör att en separat skalär avslutning undviks. Om överlappningen behöver korrigeras beror på åtgärden.
En icke-idempotent operation, till exempel en summering, skulle räkna de överlappande elementen två gånger, så maskera dem till operationens identitetselement innan de foldas in. Använd detta när varje element måste bidra exakt en gång:
// 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);
}
Den här versionen skyddar den upprullade loopen med en if, så att små datamängder hoppar över de fyra ackumulatorerna helt och går direkt till resthanteringen. När det finns tillräckligt med data ackumulerar en do/while fyra vektorer per iteration i oberoende ackumulatorer, vilket gör att processorn kan pipelinea additionerna, och kombinerar dem parvis. En switch-hopptabell bakar sedan in de återstående noll till tre hela vektorerna och, i case 0, delvektorssvansen: den återanvänder en hel vektor som förinlästs från slutet av bufferten, överlappar de element som redan har bearbetats och maskerar den överlappningen till den additiva identiteten med ConditionalSelect så att svansen förblir vektoriserad i stället för att gå över till en skalär loop. Precis som tidigare Vector128.Create läser Vector128<T>.Count element från intervallet. JIT-kompilatorn utelämnar spannets gränskontroll för typiska åtkomstmönster, så Create är ett bra standardval även i en prestandakritisk loop; LoadUnsafe (behandlas härnäst) är alternativet på lägre nivå när du itererar genom bufferten med en hanterad referens. För mycket stora indata skulle en fullständig implementering också justera bufferten och använda icke-temporala belastningar och lager för att undvika att avlägsna användbara cacherader – både utelämnade här och helt täckta av TensorPrimitives.
En idempotent-åtgärd som att söka efter ett värde kan bearbeta överlappningen på ett ofarligt sätt, så den viker in den sista vektorn direkt utan mask:
// 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;
}
Varning
Felaktig hantering av resten är en vanlig källa till buggar. En loop som läser förbi slutet av bufferten ger icke-deterministiska resultat och kan krascha. Runtimens testsvit använder en BoundedMemory-hjälpfunktion som placerar en skyddad sida direkt efter bufferten, så att alla läsningar utanför buffertens gränser utlöser en AccessViolationException under testning. Täck alltid resten av logiken, inklusive buffertar vars längd inte är en multipel av vektorbredden.
Läsa in och lagra vektorer på ett säkert sätt
För de flesta kod, Vector128.Create(span) och CopyTo är det enklaste sättet att flytta data mellan ett spann och en vektor, och JIT håller dem effektiva. När du behöver läs- och skrivåtgärder på lägre nivå – till exempel för att iterera genom en buffert med hjälp av en hanterad referens – bör du använda överlagringarna LoadUnsafe och StoreUnsafe som tar en hanterad referens och en elementförskjutning av typen nuint. Till skillnad från de pekarbaserade Load/Store-överlagrade varianterna behöver de inte att du låser bufferten, och till skillnad från rå referensaritmetik behöver du inte manuellt flytta fram en ref. Båda alternativen är lätta att få fel på sätt som introducerar skräpinsamlingshål eller åtkomstöverträdelser.
För att tomma buffertar inte ska utlösa ett undantag, hämta startreferensen från GetReference (eller GetArrayDataReference för matriser) i stället för från ref span[0].
Important
Förskjutningsaritmetik använder osignerad nuint. Kontrollera alltid buffertens längd innan du beräknar en förskjutning, till exempel buffer.Length - Vector128<int>.Count. Om bufferten är mindre än en vektor flödar subtraktionen till ett stort värde och loopen läser ogiltigt minne.
Plattformsberoende hårdvaruintrinsik
När en specifik processorinstruktion ger dig en fördel som de portabla API:erna inte erbjuder, använder du hårdvaruintrinsikerna i System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm och System.Runtime.Intrinsics.Wasm. Varje inbyggd klass har en IsSupported egenskap (även en JIT-konstant) så att du kan skydda den specialiserade sökvägen och återgå till portabel kod någon annanstans:
// 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;
}
}
Föregående metod visar hur du lyser upp kodsökvägar per arkitektur när du vill ha dem, men det är avsiktligt ett enkelt exempel: du behöver det faktiskt inte här. Det bärbara uttrycket (vector & mask) == Vector128<byte>.Zero sänker redan till den optimala instruktionen på varje plattform (till exempel ptest på x86/x64), så det gör samma arbete som de handskrivna grenarna, bara utan komplexiteten. Använd explicita intrinsikfunktioner endast när en specifik instruktion är mätbart bättre än vad de bärbara API:erna genererar.
Maskinvaruinbyggda funktioner kräver en separat implementering per instruktionsuppsättning, så behandla dem som en optimering för uppmätta heta sökvägar i stället för som standard.
Vector128
/
Vector256 API:erna är redan lägre än effektiva instruktioner på varje plattform och i praktiken vinner inte alltid avancerad kod per instruktion. Bekräfta skillnaden med ett riktmärke innan du åtar dig det extra underhållet.
Matematik på högre nivå med TensorPrimitives
Om du behöver vektoriserad matematik över spann och inte vill skriva looparna själv, tillhandahåller TensorPrimitives en stor uppsättning numeriska operationer – elementvis aritmetik, exponentialfunktioner och reduktioner som punktprodukt och cosinuslikhet – som redan är internt vektoriserade. Den är tillgänglig i NuGet-paketet 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);
För AI- och numeriska arbetsbelastningar ger TensorPrimitives ofta det mesta av nyttan med handskriven SIMD utan att medföra något av komplexiteten.
Testa alla kodsökvägar
Eftersom en vektoriserad metod har flera kodsökvägar måste testerna omfatta var och en: Vector256 sökvägen, Vector128 sökvägen och den skalära sökvägen, var och en med indata som är tillräckligt stora och för små för att gynnas. Du kan variera indatastorleken i tester, men du kan inte växla maskinvaruacceleration på testnivå. Kontrollera det i stället med miljövariabler innan processen startar:
- Ange
DOTNET_EnableAVX2=0om du vill att Vector256.IsHardwareAccelerated ska returnerafalse. - Ställ in
DOTNET_EnableHWIntrinsic=0för att helt inaktivera intrinsikfunktioner, så attVector128,Vector64ochVector<T>alla rapporterar att ingen acceleration används.
Om du vill träna varje sökväg på en enskild dator kör du testpaketet en gång utan åsidosättningar, en gång med DOTNET_EnableAVX2=0, och en gång med DOTNET_EnableHWIntrinsic=0. Alternativet är att köra på tillräckligt många olika typer av maskinvara för att täcka in dem.
Konfigurationsknappar för instruktionsuppsättningar
Utöver dessa två känner körningen igen en inställning för varje logisk gruppering av instruktionsuppsättningar, som alla har prefixet DOTNET_. Ett enda reglage kan omfatta flera relaterade instruktionsuppsättningar – EnableAVX2 till exempel styr AVX2 tillsammans med BMI1, BMI2, F16C, FMA, LZCNT och MOVBE. Att ställa in en ratt på 0 inaktiverar hela sin grupp och allt som ligger ovanpå den. Att ställa in den på 1 (standardinställningen för de flesta) tillåter gruppen, men maskinvaran måste fortfarande faktiskt ha stöd för den – att aktivera en inställning som den aktuella CPU:n saknar ignoreras, så du kan bara begränsa vad som används, aldrig tvinga fram en instruktion som inte stöds och orsaka problem.
DOTNET_EnableHWIntrinsic=0 är den stora hammaren – den stänger av allt ner till basen, så Vector128, Vector64, och Vector<T> alla rapporterar ingen acceleration och koden faller till sin programvarusökväg.
Important
Det här är diagnostiska verktyg, främst avsedda för tester och validering – att testa varje kodväg, återskapa ett maskinvaruspecifikt problem eller bekräfta en reservlösning. De är inte utformade för allmän användning eller produktionsanvändning, och de är inte ett stabilitetskontrakt. Följande uppsättning är vad .NET 11 känner igen. Tidigare versioner visade en annan uppsättning – baslinje- och AVX-512-knopparna i synnerhet konfigurerades om – så bekräfta namnen mot den körningsversion som du har som mål.
Dessa verktyg har också gränser för vad de når. Eftersom de styr vilka JIT-beslut som fattas påverkar de inte kod som redan har kompilerats i förväg via ReadyToRun eller Native AOT, och de påverkar inte heller nödvändigtvis interna rutiner som körningsmiljön och kärnbiblioteken själva använder. Behandla dem som ett sätt att styra din egen JIT-kompilerade kod, inte en global avstängningsknapp för en instruktionsuppsättning.
Grundomkopplaren och breddbegränsningarna gäller för alla arkitekturer:
Knopp (DOTNET_ prefix) |
Standardinställning | Effect |
|---|---|---|
EnableHWIntrinsic |
1 |
Huvudbrytare för alla hårdvaruintrinsikfunktioner; 0 tvingar den helt programvarubaserade körvägen. |
MaxVectorTBitWidth |
systemets standard | Begränsar Vector<T> till en maximal bitbredd. Ett värde under 128 innebär systemets standard. |
PreferredVectorBitWidth |
systemets standard | Begränsar den maximala vektorn med fast bredd som rapporterar IsHardwareAccelerated, i bitar. Ett värde under 128 innebär systemets standardvärde. |
Systemets standardvärde för MaxVectorTBitWidth kan vara snävare än vad maskinvaran har fullt stöd för, så Vector<T> utökas inte automatiskt till den bredaste tillgängliga vektorbredden. Till exempel kan Vector512<T>.IsHardwareAccelerated vara true medan Vector<T> förblir 256-bitars; ställ in DOTNET_MaxVectorTBitWidth=512 för att låta Vector<T> använda den bredare bredden.
PreferredVectorBitWidth begränsar den maximala vektorbredden som rapporterar IsHardwareAccelerated. Om värdet sänks under vad hårdvaran stöder inaktiveras de större vektorbredderna: på en maskin som stöder 512-bitarsvektorer gör Vector512<T>.IsHardwareAccelerated att DOTNET_PreferredVectorBitWidth=256 rapporterar false. Det är en allmän inställning, men i dag är det bara x86/x64 som stöder bredder över 128, så det är bara där den får en märkbar effekt.
Varje logisk gruppering av x86/x64-instruktionsuppsättningar har också en egen växel:
Knopp (DOTNET_ prefix) |
Standardinställning | Gates |
|---|---|---|
EnableAVX |
1 |
AVX och beroenden |
EnableAVX2 |
1 |
AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE och beroenden |
EnableAVX512 |
1 |
AVX-512 F+BW+CD+DQ+VL och beroenden |
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 (utökade allmänna ändamålsregister) |
EnableAES |
1 |
AES, PCLMULQDQ |
EnableAVX512VP2INTERSECT |
1 |
AVX-512 VP2INTERSECT |
EnableAVXIFMA |
1 |
AVX-IFMA |
EnableAVXVNNI |
1 |
AVX-VNNI |
EnableAVXVNNIINT |
1 |
VEX AVX–VNNI-INT8 och AVX –VNNI-INT16 |
EnableGFNI |
1 |
GFNI |
EnableSHA |
1 |
SHA |
EnableVAES |
1 |
VAES, VPCLMULQDQ |
EnableWAITPKG |
1 |
WAITPKG |
EnableX86Serialize |
1 |
X86 SERIALIZE |
På Arm64 har varje logisk gruppering av instruktionsuppsättningar sin egen växel:
Knopp (DOTNET_ prefix) |
Standardinställning | Gates |
|---|---|---|
EnableArm64Aes |
1 |
AES |
EnableArm64Atomics |
1 |
Atomiska operationer för stora systemtillägg (LSE) |
EnableArm64Crc32 |
1 |
CRC32 |
EnableArm64Dczva |
1 |
DC ZVA nollställning av cache |
EnableArm64Dp |
1 |
Punktprodukt |
EnableArm64Rdm |
1 |
Avrundande fördubblande multiplicera-ackumulera (RDM) |
EnableArm64Sha1 |
1 |
SHA1 |
EnableArm64Sha256 |
1 |
SHA256 |
EnableArm64Rcpc |
1 |
Versionskonsekvent, processorkonsekvent beställning (RCpc) |
EnableArm64Rcpc2 |
1 |
RCpc2 |
EnableArm64Cssc |
0 |
Vanlig kortsekvenskomprimering (CSSC) |
EnableArm64Sve |
1 |
SVE (Scalable Vector Extension) |
EnableArm64Sve2 |
1 |
SVE2 |
EnableArm64Sha3 |
1 |
SHA3 |
EnableArm64Sm4 |
1 |
SM4 |
EnableArm64SveAes |
1 |
SVE AES |
EnableArm64SveSha3 |
1 |
SVE SHA3 |
EnableArm64SveSm4 |
1 |
SVE SM4 |
En inställning med standardvärdet 0 (till exempel EnableAVX10v2 eller EnableArm64Cssc) aktiverar en instruktionsuppsättning som fortfarande håller på att tas i bruk, så den förblir avstängd tills du väljer att aktivera den.
Kör ett prestandatest för att bekräfta framgången
Vektorisering ökar komplexiteten, så mät att den lönar sig innan du behåller den. Använd BenchmarkDotNet och använd samma miljövariabler som visades tidigare för att jämföra skalära, Vector128och Vector256 implementeringar i en körning. BenchmarkDotNets demonteringsdiagnoser kan också generera den genererade sammansättningen, vilket är ovärderligt vid justering av kod med höga prestanda.
Några saker att tänka på:
- Större indata har större nytta. För små buffertar kan vektoriserad kod vara långsammare än skalär kod på grund av installationskostnader. Jämför de indatastorlekar som anroparna faktiskt använder.
- Prestandaförbättringar är sällan perfekta. En 256-bitars vektor som arbetar med 32-bitars element blir inte nödvändigtvis 8 gånger snabbare; minnesbandbredd, alignering och instruktionslatens spelar alla in.
- Minnesjustering påverkar stabiliteten. Slumpmässig allokeringsjustering lägger till brus mellan körningar. Du kan allokera justerat minne med AlignedAlloc för stabila resultat eller aktivera BenchmarkDotNets minnes slumpmässighet för att observera den fullständiga fördelningen.
Metodtips
- Sök efter befintliga API:er på högre nivå först.
Span<T>,string, LINQ,TensorPrimitivesoch tensortyperna påskyndar redan många vanliga operationer åt dig – implementera inte själv sådant som redan är optimerat och testat. - Börja med
Vector128<T>; det har acceleration på det bredaste urvalet av maskinvara, och du behöver inteVector256<T>för att få en korrekt, portabel implementering. Lägg endast till större bredder och hårdvaruintrinsik för uppmätta kritiska kodvägar. - Kontrollera
IsHardwareAcceleratedochCountdirekt i stället för att cachelagra dem. JIT omvandlar dem till konstanter. - Hantera alltid resten av loopen och ta hänsyn till överlappande käll- och målbuffertar vid lagring.
- Skriv edge-case-tester först, sedan en skalär lösning och uttryck sedan den skalära logiken med vektor-API:erna.
- Testa varje kodsökväg (inklusive åtkomstöverträdelser) och jämföra realistiska indatastorlekar innan du genomför den extra komplexiteten.