A SIMD és a hardverintrinzikek használata a .NET-ben

A SIMD (egyetlen utasítás, több adat) hardveres támogatás egy művelet több adatra való alkalmazásához egyetlen utasítással párhuzamosan. A vektorizált kód iterációnként több értéket dolgoz fel egy helyett, ami jelentősen növelheti az átviteli sebességet a numerikus, tudományos, grafikus, szövegfeldolgozási és adat-párhuzamos munka esetében, ahol ugyanaz a művelet ismétlődik egy pufferen keresztül. Ennek az ára a nagyobb összetettség, ezért leginkább akkor éri meg, ha a bemenet kellően nagy, és a nyereséget mérések is igazolják.

.NET többféle SIMD-támogatást biztosít. Azt válassza, amelyik megfelel annak, hogy mekkora irányításra van szüksége, és mekkora összetettséget hajlandó vállalni.

SIMD-támogatás típusai a .NET

API Namespace Mikor érdemes használni?
Rögzített célú vektorok és mátrixtípusok System.Numerics Grafikus és geometriai matematika 2-4 elemvektorokkal, mátrixokkal, kvartettekkel és síkokkal.
Vector<T> System.Numerics Hordozható, változószélességű vektorosítás, ha nincs szükség platformonkénti vezérlésre.
Vector64<T>, Vector128<T>, Vector256<T>Vector512<T> System.Runtime.Intrinsics Platformfüggetlen, rögzített szélességű vektorizálás részletes vezérléssel. Ez az új vektorizált algoritmusok ajánlott kiindulópontja.
Hardveres belső metrikák \, \, \ Olyan speciális processzorutasítások, amelyeket a magasabb szintű API-k nem tesznek elérhetővé, de amelyekkel a lehető legnagyobb teljesítmény nyerhető ki egy kritikus futási útvonalon.
TensorPrimitives System.Numerics.Tensors Kész, vektorizált matematikai átnyúlások. Elvégzi a vektorizálást.

Ahol ezek az API-k átfedésben vannak, azok absztrakciós rétegeken keresztül kapcsolódnak. Az általános vektortípusok azok az alapvető adatcseretípusok, amelyeket a többi réteg egymás között továbbad, ezért technikailag ezek a legalacsonyabb szintűek: a változó szélességű Vector<T>, amely akkorára bővül, amekkora szélességet a futó hardver támogat, valamint a rögzített szélességű Vector64<T>Vector512<T>. A System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm és System.Runtime.Intrinsics.Wasm elemekben található platformspecifikus hardveres intrinzikek ezeken a típusokon működnek, és mindegyik közvetlenül egy-egy processzorutasításnak felel meg. Az általános típusokon elérhetővé tett, platformfüggetlen műveletek egy szinttel a platformspecifikus intrinzikek felett helyezkednek el, és célonként azokra fordulnak le. Még magasabb szinten vannak a teljes puffereken működő felügyelt API-k – a Span<T>-n és string-n elérhető vektorizált metódusok, valamint a TensorPrimitives –, amelyek az alattuk lévő rétegekre épülnek, így kézzel írt megvalósítás nélkül élvezheti a SIMD-gyorsítást. A System.Numerics rögzített alakú típusok a grafika és a geometria területén használt, tartományspecifikus kényelmi típusok, nem pedig ennek az adatcsereveremnek a részei.

A cikk további része ezen API-kon keresztül működik a legmagasabb szinttől a legalacsonyabbig, majd a teszteléssel, a teljesítményértékeléssel és az ajánlott eljárásokkal foglalkozik.

System.Numerikus vektorok és mátrixtípusok

A System.Numerics névtér SIMD-gyorsított típusokat biztosít rögzített alakzattal:

Ezek a típusok természetes módon illeszkednek a grafikához és a geometriához, és a futtatókörnyezet SIMD-utasításokkal gyorsítja fel a műveleteiket, ahol ezt a hardver támogatja. Az alábbi példa két vektort ad hozzá:

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

Emellett elérhetővé teszik a várt gyakori vektoros matematikai adatokat is, például a pont szorzatát, a távolságot és a befogást:

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

A mátrixtípusok támogatják a mátrix matematikát, például a transzponálást és a szorzást:

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> egy primitív numerikus típus változószélességű vektorát jelöli. A hossza a folyamat élettartamára van rögzítve, de az érték Vector<T>.Count a kódot futtató processzortól függ. A Just-In-Time (JIT) fordító állandóként kezeli Count , így a rá írt hurkok jól optimalizálhatók.

Vector<T> lehetővé teszi a hordozható vektorizációt platformonkénti kód nélkül, annak árán, hogy nem ismeri a vektor szélességét a fordítási időpontban. Az alábbi példa két tömb elemalapú hozzáadását számítja ki:

// 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;
}

Megjegyzés:

Ez a példa szemléltető jellegű. Ritkán kell kézzel írnia egy ilyen hurkot, mert TensorPrimitives már gyorsított span-alapú matematikát biztosít. Ez az elemenkénti összeadás Add, és az olyan redukciós műveletek is biztosítottak, mint a Sum. Ezek a műveletek hardveresen gyorsulnak a (Vector<T>) elemet támogató elemtípusok Vector<T>.IsSupported esetében.

Hardveres gyorsítás ellenőrzése

A SIMD-gyorsított típusok még olyan hardveres vagy JIT-konfigurációkon is működnek, amelyek nem támogatják a SIMD-t, mert visszaállnak a nem gyorsított szoftveres implementációkra. Annak megállapításához, hogy a gyorsítás valóban elérhető-e, ellenőrizze a megfelelő IsHardwareAccelerated tulajdonságot:

Ezeket a tulajdonságokat a JIT állandókká alakítja, így a nem használt ágak megszűnnek, és nincs futásidejű költség az ellenőrzésükhöz. Ne gyorsítótárold az értékeket; közvetlenül ott olvasd ki őket, ahol szükség van rájuk. Ugyanez vonatkozik a Count tulajdonságokra (például Vector128<T>.Count), amelyek JIT-időállandók is.

A gyorsított szélességen végzett műveletek többsége szintén gyorsított, de ez nem minden művelet esetében garantált. Például a lebegőpontos osztás gyorsabb lehet, míg az egész osztás nem. Ha a Vector256 gyorsított, akkor a Vector128 is általában az, de erre nincs garancia, ezért ellenőrizzen minden használt szélességet.

Jótanács

Ha egy szükséges műveletet nem gyorsít fel egy fontos platformon, vagy új platformfüggetlen API-t szeretne, küldjön egy problémát a dotnet/runtime szolgáltatásban. Ugyanez vonatkozik a kódgenerálási fejlesztésekre is.

Nem minden elemtípus érvényes minden vektorra. Vector128<T>és testvérei támogatják a primitív numerikus típusokat (byte, , sbyte, shortushort, intuint, long, ulong, float, , double, nint, és nuint) ma, és ez a készlet a jövőben más típusokat is tartalmazhat. Annak meghatározására szolgál Vector128<T>.IsSupported , hogy egy adott T érvényes-e, ami különösen hasznos az általános kódból.

A nem támogatott típusok( például char és bool) továbbra is vektorizálhatók a puffer azonos méretű támogatott típusként történő újraértelmezésével. A Cast használatával újraértelmezhet egy tartományt – például char-tól ushort-ig –, vagy a vektor As<TFrom, TTo> metódusával egy már meglévő vektort értelmezhet újra. Az újraértelmezés csak a típust módosítja, a mögöttes biteket nem, ezért az Ön felelőssége, hogy az adatok megfelelően legyenek formázva: egynek bool meg kell maradnia 0 , vagy 1, és egy char érvényes UTF-16 kódegységnek kell maradnia. Ha egy vektorizált művelet tartományon kívüli értéket eredményezne, a visszaírás előtt ügyeljen arra, hogy normalizálja az eredményt.

Platformfüggetlen vektorizáció a Vector128 használatával

Vector128<T> a vektorizálást támogató összes platform közös nevezője, ezért ez a legjobb kiindulópont. 128 bites vektort tartalmaz: 16 bájt, 8 rövid, 4 ints/float vagy 2 hosszú/dupla.

------------------------------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> kétszer olyan széles, Vector512<T> pedig annál is kétszer szélesebb. Nem minden hardver támogatja a nagyobb szélességeket, ezért az alábbi példák a hordozhatóság érdekében használhatók Vector128 .

Minden szélességhez tartozik egy általános típus (Vector128<T>) az adatokhoz, és egy nem általános statikus osztály (Vector128), amely a legtöbb műveletet tartalmazza, beleértve a statikus gyári metódusokat, például Create és Load. Az olyan operátorok, mint +a , &, és << az aritmetikai és bitműveletek kifejezésének idiomatikus módja; előnyben részesítik őket a nevesített módszer megfelelőivel szemben, hogy elkerüljék az operátorok előnyben részesítési hibáit, és javítsák az olvashatóságot. A bájtsorrendtől függő algoritmusok esetében használjon a(z) IsLittleEndian alapján elágazást, amelyet a JIT szintén konstanssá egyszerűsít.

Megjegyzés:

X86/x64 Vector256<T> rendszeren a műveletek általában két független, 128 bites "sávként" vannak kezelve. A legtöbb elemalapú művelet esetében ez transzparens, de a sávok közötti műveletek (például a shuffles vagy a párirányú/vízszintes műveletek) eltérően viselkedhetnek, vagy az egyenértékűnél Vector128 többe kerülhetnek. Erősítse meg a teljesítménytesztekkel, mielőtt feltételezi, hogy a szélesebb vektor gyorsabb.

A sávátlépési műveletek nem szélesednek ingyenesen

Az elemalapú műveletek nem törődnek a szélességekkel: v1 + v2 ugyanaz az elemenkénti eredmény, függetlenül attól, hogy vannak-e v1v2Vector128<T> vagy Vector256<T>– a szélesítés csak egy sávnyi adategységet dolgoz fel utasításonként. A(z) Add a(z) v = [a, b, c, d] és w = [e, f, g, h] elemein mindig azonos indexű elemeket kombinál:

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

A sávátlépési műveletek nem skálázhatók fel egyszerűen, mivel az egyesítendő elemek a vektor szélességétől függenek. A párirányú csökkentés egyesíti a szomszédos elemeket az azonos indexű elemek helyett, így a szélesítés megváltoztatja a párosított elemeket:

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)

Pontosan ezt teszi a vízszintes csökkentés: összegzi a vektor elemeit két ciklikus párirányú összeadással. X86/x64 Vector128<float> rendszeren (4 elem) két hívással HorizontalAddérhető el:

// 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();
}

Ugyanezt a két hívásból álló mintát alkalmazva a Vector256<float>-re (8 elem), jónak tűnik, de nem az. HorizontalAdd nem működik a teljes 256 bites vektoron; a 128 bites sávon belül egymástól függetlenül ismétli a páronkénti mintát. Két kör eredményeként az alsó sáv összege (0–3. elem) az alsó sáv minden elemére kiterjesztve, illetve a felső sáv összege (4–7. elem) a felső sáv minden elemére kiterjesztve adódik, nem pedig mind a nyolc elem összege:

// 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();
}

A helyes végösszeg meghatározásához explicit módon lépje át a sávhatárt: olvassa ki az egyes sávok részösszegét a(z) GetLower/GetUpper segítségével, majd adja őket össze — a(z) GetLower és GetUpper a vektort az első és a második felére bontja:

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

Ez a plusz lépés a sávok keresztezésének valós költsége. Egy sávkeresztező algoritmus nem lesz csak úgy „ingyen” szélesebb, úgy, ahogy egy elemenkénti algoritmus; mérjen, mielőtt azt feltételezi, hogy a szélesebb vektor nyer.

Gyakori műveletek

Vector128 és szélesebb testvérei nagy API-felületet fednek fel. Nem kell megjegyeznie– ismeri a kategóriákat, és szükség esetén megkeresi a részleteket. Minden művelethez tartozik szoftveres tartalékmegoldás az olyan platformokhoz, amelyek nem tudják azt gyorsítani. Az alábbi táblázat lényegében az egész felületet lefedi.

Kategória Mire szolgál? Reprezentatív API-k
Constants Előre definiált állandó vektorok Zero, One, NegativeOne, AllBitsSet, Indices, SignSequence, E, Pi, Tau, Epsilon, NaN, PositiveInfinity, NegativeInfinity, NegativeZero
Létrehozása Skalár közvetítése, elemek beállítása vagy sorozat létrehozása Create, CreateScalar, CreateScalarUnsafe, Create(ReadOnlySpan<T>)CreateSequence, CreateGeometricSequence, CreateHarmonicSequenceCreateAlternatingSequence
Betöltés és tárolás Adatok áthelyezése a memória és a vektor között Load, LoadUnsafe, LoadAligned, LoadAlignedNonTemporal, StoreStoreUnsafe, StoreAligned, StoreAlignedNonTemporal, , CopyToTryCopyTo
Arithmetic Elemalapú matematikai és csökkentési műveletek Add (x + y), Subtract (x - y), Multiply (x * y), Divide (x / y), Negate (-x), AddSaturate, SubtractSaturate, Abs, Sqrt, FusedMultiplyAdd, Dot, Sum
Bitműveletek Bitenkénti logikai műveletek és eltolások BitwiseAnd (x & y), BitwiseOr (x \| y), Xor (x ^ y), AndNot (x & ~y)OnesComplement (~x), ShiftLeft (x << n), ShiftRightArithmetic (x >> n)ShiftRightLogical (x >>> n)
Minimum, maximum és korlátozás Elemenkénti minimum-, maximum- és tartománykorlátozás Min, Max, Clamp, MinMagnitudeMaxMagnitude, MinNumber, MaxNumber, , MinMagnitudeNumberMaxMagnitudeNumber
Kerekítés Minden elem kerekítése egy integrál értékre Ceiling, Floor, RoundTruncate
Matematikai függvények Jel, interpoláció, szög és transzcendentális segítők CopySign, Lerp, DegreesToRadians, RadiansToDegrees, Hypot, Sin, Cos, SinCos, Asin, Exp, Log, Log2
Comparison Az egyes elemek összehasonlítása; az eredmény vektormaszk, nem pedig az bool operátor által megadott Equals, GreaterThan, GreaterThanOrEqual, LessThanLessThanOrEqual
Classification Elemenkénti predikátumok a számsoron, mindegyik vektormaszkot ad vissza IsNaN, IsFinite, IsInfinity, IsPositiveInfinity, IsNegativeInfinity, IsInteger, IsEvenInteger, IsOddInteger, IsNegative, IsPositive, IsNormal, IsSubnormal, IsZero
Összehasonlítási csökkentések Elemenkénti összehasonlítás összecsukása egyetlen elemhez bool EqualsAll (x == y), EqualsAny, GreaterThanAll, GreaterThanAny, GreaterThanOrEqualAllGreaterThanOrEqualAny, LessThanAll, LessThanAny, , LessThanOrEqualAllLessThanOrEqualAny
Egészvektor-predikátumok Vektor redukálása bool-ra: hogy az összes, bármelyik vagy egyik elem sem egyenlő egy adott értékkel, vagy (a WhereAllBitsSet változatok esetén) minden bit be van állítva. Ezeket előnyben részesítse a maszk indexké alakításával szemben All, Any, None, AllWhereAllBitsSetAnyWhereAllBitsSetNoneWhereAllBitsSet
Kereső Elemek megszámlálása vagy megkeresése érték szerint, vagy (az WhereAllBitsSet űrlapok) sávok beállítása maszkban Count, IndexOf, LastIndexOf, CountWhereAllBitsSetIndexOfWhereAllBitsSetLastIndexOfWhereAllBitsSet
Maszk az indexeléshez Összehasonlító maszk skaláris bitmaszkká alakítása és vizsgálata ExtractMostSignificantBits TrailingZeroCount-val vagy LeadingZeroCount-vel
Szelekció Két vektor bitenkénti keverése egy maszk alapján ConditionalSelect(x, y, z), egyenértékű a (y & x) \| (z & ~x)
Conversion Numerikus típus módosítása, új értékek számítása (például intfloat) ConvertToInt32, ConvertToInt64, ConvertToUInt32, ConvertToUInt64ConvertToSingleConvertToDouble
Szélesítés és szűkítés Az elemek felosztása szélesebb típusba, vagy becsomagolása egy szűkebbe Widen, WidenLower, WidenUpper, NarrowNarrowWithSaturation
Újraértelmezés A bitek újraértelmezése egy másik elemtípusként anélkül, hogy módosítaná őket As<TFrom, TTo>, AsByte, AsInt32, AsSingle, és a többi As* elemforma
System.Numerics interop Újraértelmezés Vector128<T> és a rögzített alakú numerikus típusok között AsVector, AsVector2, AsVector3, AsVector4AsPlane, AsQuaternion, AsVector128AsVector128Unsafe
Újrarendelés Elemek átrendezése index alapján vagy két vektor egymásba fonása Shuffle, Reverse, Zip, ZipLower, ZipUpper, Unzip, UnzipEven, UnzipOdd, ConcatLowerLower, ConcatLowerUpper, ConcatUpperLower, ConcatUpperUpper
Sávhozzáférés Egyes elemek és felérek olvasása vagy cseréje, illetve a vektor átméretezése GetElement, WithElement, ToScalar, GetLowerGetUpper, WithLower, WithUpperToVector256

Jótanács

Számos műveletnek van Estimate és Native változata – például: MultiplyAddEstimate, ClampNative, MinNative, MaxNative, ShuffleNative és ConvertToInt32Native. Ezek egy gyorsabb hardverutasításra képeződnek le, amely némi pontosságot feláldoz, vagy lemond egy IEEE által garantált speciális esetre vonatkozó biztosítékról (például a NaN-kezelésről), ezért csak akkor érdemes használni őket, ha egy teljesítményteszt azt mutatja, hogy a pontos változat a szűk keresztmetszet, és a lazább szemantika elfogadható.

Megjegyzés:

Vector256.Shuffle a bemenetét egyetlen 256 bites vektorként kezeli, ahol a platformspecifikus Avx2.Shuffle két független 128 bites sávként működik. A platformfüggetlen API a hordozhatóbb választás, de erősítse meg, hogy milyen viselkedésre van szükség a kézzel írt belső eszközök portolása során.

A kód elérési útjának felépítése

A vektorizált metódus általában vektorszélességenként egy-egy útvonalra bomlik, valamint tartalmaz egy skaláris tartalékmegoldást a kis bemenetekhez és a gyorsítás nélküli hardverhez. A hardver által támogatott legnagyobb vektor használatához először ellenőrizze a legszélesebb vektort, és dolgozzon le:

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

Az egyes szélességek külső feltétele a Vector128.IsHardwareAccelerated elemet (egy JIT-időben ismert állandót arról, hogy a platform gyorsítja-e az adott szélességet) a Vector128<T>.IsSupported elemmel kombinálja (arról, hogy a T elemtípus érvényes-e az adott szélességhez). Egy támogatott blokkon belül hasonlítsa össze a bemenet hosszát a(z) Count értékkel annak eldöntéséhez, hogy a vektorizált ágat vagy a kis bemenetekhez használt tartalék megoldást válassza. A metódus a(z) T típusparaméterre generikus, és a Vector256 és Vector512 blokkok – amelyek megegyeznek a Vector128 blokkal, de a nagyobb adattípust használják – a rövidség kedvéért ki vannak kommentezve.

Két különálló tartalék megoldás van. A még a legszűkebb vektorhoz is túl kicsi puffer gyorsított hardveren a SumVectorSmall-hoz fordul — egy explicit switch ugrótáblához, amely ciklus nélkül kezeli az összes lehetséges részvektorhosszt:

// 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) JIT-időállandó is, ezért SumVectorSmall az elemszélességen a legszélesebb vektor elemértékének megfelelő táblázatba küldi az elemeket – ugyanezt a megközelítést TensorPrimitives használja. (Ha az új memóriabiztonsági modell engedélyezve van, sizeof(T) a korlátozással rendelkező unmanaged típusparaméteren lévő kifejezések engedélyezettek a biztonságos kódban.) Csak a 4 bájtos tábla jelenik meg; az 1 bájtos, a 2 bájtos és a 8 bájtos táblák megosztják az alakzatát. A nagyobb esetekben a maradékot egy Vector256 vagy Vector128 használatával, két egymást átfedő betöltéssel kezelik — az egyikkel az elejéről, a másikkal a végéről —, így a kihagyott Vector512/Vector256 útvonalakhoz tartozó, szélesebb maradék kezelését magában az ugrótáblában oldják meg. A két betöltés átfedi egymást, amikor a hossz nem a szélesség pontos egész számú többszöröse, ezért a maradék részt az additív identitásra maszkolják ConditionalSelect segítségével, mielőtt összegzik. Ez a maszk csak azért szükséges, mert a hozzáadás nem idempotens; egy idempotens művelet, például a keresés közvetlenül összecsukhatja az átfedésben lévő farokrészt. A hardveren lévő puffer, amely egyáltalán nem rendelkezik vektorizációval, egy szokásos skaláris hurokba SumScalarkerül.

Iteráljon végig a bemeneten, és kezelje a maradékot

Ha egy vektornál nagyobb puffert szeretne feldolgozni, akkor egyszerre egy vektort kell átölelnie, majd kezelnie kell azokat a maradék elemeket, amelyek nem töltik ki a teljes vektort. A maradék elemek kezelésének robusztus módja egy utolsó, teljes vektornyi elem újrafeldolgozása, így átfedésbe kerülve néhány, a ciklus által már feldolgozott elemmel — ami elkerüli egy különálló skaláris epilógus szükségességét. Az, hogy az átfedést javítani kell-e, a művelettől függ.

Egy nem idempotens művelet, például az összegzés, kétszer számolná meg az átfedő elemeket, ezért maszkoljuk őket a művelet identitáselemére, mielőtt bevonnánk őket az összesítésbe. Ezt akkor használja, ha minden elemnek pontosan egyszer kell közreműködnie:

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

Ez a verzió egy if feltétellel védi a kigöngyölített ciklust, így a kis méretű bemenetek teljesen átugorják a négy akkumulátort, és közvetlenül a maradék feldolgozására térnek át. Ha elegendő adat áll rendelkezésre, a do/while rendszer iterációnként négy vektort halmoz fel független gyűjtőkbe – ez lehetővé teszi a processzor számára az összeadások folyamatát –, és párosítással egyesíti őket. Egy switch ugrótábla ezután kezeli a fennmaradó nullától háromig terjedő számú teljes vektort, valamint a case 0 esetben az alvektoros maradékot is: újra felhasznál egy, a puffer végéről előre betöltött teljes vektort, amely átfedi a már feldolgozott elemeket, és az átfedő részt ConditionalSelect segítségével az additív identitásra maszkolja, így a maradék feldolgozása vektorizált marad ahelyett, hogy egy skaláris ciklusra váltana. Ahogyan korábban, a Vector128.Create a spanből olvassa ki a Vector128<T>.Count elemeket. A JIT a span határellenőrzését elhagyja a tipikus hozzáférési minták esetén, így a Create jó alapértelmezett választás még kritikus teljesítményű ciklusban is; a LoadUnsafe (amelyről a következő részben lesz szó) az alacsonyabb szintű alternatíva arra az esetre, ha a puffert felügyelt referencián keresztül járod be. Nagyon nagy bemenetek esetén a teljes implementáció a puffert is igazítaná, és nem időleges betöltési és tárolási műveleteket használna, hogy ne szorítsa ki a hasznos gyorsítótársorokat – mindkettőt kihagytuk innen, és ezeket TensorPrimitives tárgyalja teljes körűen.

Egy idempotens művelet, például egy érték keresése, káros hatás nélkül újrafeldolgozhatja az átfedő részt, ezért az utolsó vektort közvetlenül, maszk nélkül veszi be:

// 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

A maradék helytelen kezelése gyakori hibaforrás. A puffer végén túl olvasó ciklus nemdeterminisztikus eredményeket okoz, és összeomláshoz vezethet. A futtatókörnyezet tesztkészlete egy BoundedMemory segédet használ, amely közvetlenül a puffer után helyez el egy hozzáférhetetlen lapot, így minden pufferhatáron túli olvasás AccessViolationException kivételt vált ki a tesztelés során. Mindig fedje le a fennmaradó logikát, beleértve azokat a puffereket is, amelyek hossza nem a vektorszélesség többszöröse.

Vektorok biztonságos betöltése és tárolása

A legtöbb kód esetében a Vector128.Create(span) és a CopyTo a legegyszerűbb módja az adatok span és vektor közötti mozgatásának, és a JIT gondoskodik róla, hogy hatékonyak maradjanak. Ha alacsonyabb szintű betöltési és tárolási műveletekre van szüksége – például ahhoz, hogy egy pufferen felügyelt hivatkozással végighaladjon –, részesítse előnyben a LoadUnsafe és StoreUnsafe túlterheléseket, amelyek felügyelt hivatkozást és nuint elemeltolást fogadnak el. A mutatóalapú Load/Store túlterhelésekkel ellentétben ezeknél nem kell rögzíteni a puffer helyét, és a nyers referenciaaritmetikával ellentétben nem kell kézzel továbbléptetni egy ref-t. Mindkét alternatívát könnyű úgy elrontani, hogy az a szemétgyűjtő működésében rést vagy hozzáférés-megsértési hibát okozzon.

Annak érdekében, hogy az üres pufferek ne dobjanak kivételt, a kezdő referenciát a GetReference-ből (vagy tömbök esetén a GetArrayDataReference-ből) szerezze be, ne a ref span[0]-ből.

Important

Az eltolásaritmetika előjel nélküli nuint használ. Mindig ellenőrizze a puffer hosszát, mielőtt kiszámítana egy eltolást, például a(z) buffer.Length - Vector128<int>.Count értéket. Ha a puffer kisebb, mint egy vektor, akkor a kivonás nagy értékre csökken, és a ciklus érvénytelen memóriát olvas be.

Platformspecifikus hardveres belső metrikák

Ha egy adott processzorutasítás olyan előnyt kínál, amelyet a hordozható API-k nem tesznek elérhetővé, használja a hardverintrinsicseket a System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm és System.Runtime.Intrinsics.Wasm esetében. Minden belső osztály rendelkezik egy IsSupported tulajdonságdal (jiT-állandóval is), így védheti a speciális útvonalat, és máshová is visszatérhet a hordozható kódhoz:

// 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;
    }
}

Az előző módszer bemutatja, hogyan világosíthatja fel az architektúránkénti kódútvonalakat, amikor szeretné őket, de ez szándékosan egy egyszerű példa: itt valójában nincs rá szüksége. A hordozható kifejezés (vector & mask) == Vector128<byte>.Zero már eleve az optimális utasításra csökken minden platformon (például ptest x86/x64 esetén), így ugyanúgy működik, mint a kézzel írt ágak, egyszerűen az összetettség nélkül. Csak akkor nyúljon explicit intrinsics függvényekhez, ha egy adott utasítás mérhetően jobb teljesítményt nyújt annál, mint amit a hordozható API-k generálnak.

A hardverintrinzikek utasításkészletenként külön implementációt igényelnek, ezért ne alapértelmezettként tekintsen rájuk, hanem a méréssel azonosított kritikus végrehajtási útvonalak optimalizálásaként. Az Vector128/Vector256 API-k már alacsonyabbak a hatékony utasításokhoz az egyes platformokon, és a gyakorlatban a kifinomult utasításonkénti kód nem mindig nyer. Mielőtt véglegesíteni szeretné a további karbantartást, ellenőrizze a különbséget egy referenciamutatóval.

Magasabb szintű matematika TensorPrimitives használatával

Ha vektoros matematikára van szüksége, és nem szeretné saját maga megírni a hurkokat, TensorPrimitives számos numerikus művelettel rendelkezik – elemalapú aritmetikai, exponenciális és csökkentési lehetőségeket, például pontalapú szorzatot és koszinusz-hasonlóságot –, amelyek már belsőleg vektorizálva vannak. A System.Numerics.Tensors NuGet csomagban érhető el.

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

MI- és numerikus feladatok esetén a TensorPrimitives gyakran a kézzel írt SIMD előnyeinek nagy részét biztosítja, annak bonyolultsága nélkül.

Az összes kódútvonal tesztelése

Mivel egy vektorizált metódus több kódútvonallal is rendelkezik, a teszteknek mindegyikre ki kell terjedniük: az Vector256 elérési útra, az Vector128 elérési útra és a skaláris útvonalra, és mindegyik olyan bemenettel rendelkezik, amely elég nagy és túl kicsi ahhoz, hogy hasznot húzjon. A tesztekben módosíthatja a bemeneti méretet, de a hardveres gyorsítást nem válthatja ki a teszt szintjén. Ehelyett a folyamat megkezdése előtt vezérelje a környezeti változókkal:

  • Állítsa DOTNET_EnableAVX2=0 értékét úgy, hogy a(z) Vector256.IsHardwareAcceleratedfalse értéket adjon vissza.
  • Állítsa DOTNET_EnableHWIntrinsic=0 értékét erre az intrinsic függvények teljes letiltásához, így Vector128, Vector64 és Vector<T> mind azt jelzi, hogy nincs gyorsítás.

Az összes útvonal lefedéséhez egyetlen gépen futtassa a tesztkészletet egyszer felülbírálások nélkül, egyszer a DOTNET_EnableAVX2=0 használatával, és egyszer a DOTNET_EnableHWIntrinsic=0 használatával. Az alternatíva az, hogy kellően sokféle hardveren futtatjuk, hogy lefedjük őket.

Utasításkészlet konfigurációs gombjai

E kettőn túl a futtatókörnyezet az utasításkészletek minden egyes logikai csoportjához egy kapcsolót ismer fel, mindegyik DOTNET_ előtaggal. Egyetlen gomb több kapcsolódó utasításkészletet is képes lefedni–EnableAVX2 például az AVX2 kapukat, valamint a BMI1, BMI2, F16C, FMA, LZCNT és MOVBE kapukat. Ha egy gombot 0 értékre állít, az letiltja a teljes csoportját és minden fölé rétegzett elemet. Ha ezt a(z) 1 értékre állítja (ami a legtöbb esetben az alapértelmezés), az engedélyezi a csoportot, de a hardvernek továbbra is ténylegesen támogatnia kell azt – a rendszer figyelmen kívül hagyja egy olyan beállítás engedélyezését, amelyet az aktuális CPU nem támogat, így legfeljebb csak a használtakat szűkítheti, nem pedig nem támogatott utasítást kényszeríthet ki és teheti működésképtelenné a rendszert. DOTNET_EnableHWIntrinsic=0 a durva megoldás — a legalapvetőbb szintig mindent kikapcsol, így a Vector128, Vector64 és Vector<T> egyaránt azt jelzi, hogy nincs gyorsítás, a kód pedig a szoftveres végrehajtási útvonalra tér át.

Important

Ezek olyan diagnosztikai eszközök, amelyek elsősorban tesztelésre és ellenőrzésre szolgálnak – az egyes kódelérési utak gyakorlása, egy hardverspecifikus probléma reprodukálása vagy a tartalék állapot megerősítése. Nem általános vagy éles használatra készültek, és nem jelentenek stabilitási garanciát. A következő készletet .NET 11 felismeri; a korábbi kiadások egy másik készletet tettek közzé – az alapkonfigurációt és különösen az AVX-512-es gombokat újrakonfigurálták –, ezért erősítse meg a neveket a megcélzott futtatókörnyezeti verzióval.

Ezek az eszközök azt is korlátozzák, hogy mit érnek el. Mivel a JIT döntéseit szabályozzák, nem érintik a ReadyToRun vagy a Native AOT használatával már előre lefordított kódot, és nem feltétlenül érintik azokat a belső rutinokat sem, amelyeket maga a futtatókörnyezet és az alapkönyvtárak használnak. Tekintsen rájuk úgy, mint a saját, JIT-tel fordított kód vezérlésére szolgáló eszközökre, ne pedig mint egy utasításkészlet globális ki-/bekapcsolójára.

Az alapkapcsoló és a szélességi korlát minden architektúrára érvényes:

Gomb (DOTNET_ előtag) Default Effect
EnableHWIntrinsic 1 Főkapcsoló az összes hardveres intrinsicshöz; a 0 a teljesen szoftveres útvonal használatát kényszeríti ki.
MaxVectorTBitWidth rendszer alapértelmezett A bitek Vector<T> maximális szélességét bitben korlátozza; a 128 alatti érték a rendszer alapértelmezése.
PreferredVectorBitWidth rendszer alapértelmezett Korlátozza a IsHardwareAccelerated által jelentett maximális rögzített szélességű vektort bitekben; a 128 alatti érték a rendszer alapértelmezése.

A rendszer alapértelmezett értéke MaxVectorTBitWidth szűkebb lehet, mint amit a hardver teljes mértékben támogat, így Vector<T> nem nő automatikusan a legszélesebb rendelkezésre álló vektorra. Például a Vector512<T>.IsHardwareAccelerated lehet true, miközben a Vector<T> 256 bites marad; a DOTNET_MaxVectorTBitWidth=512 beállításával a Vector<T> a nagyobb szélesség használatára kényszeríthető.

PreferredVectorBitWidth korlátozza a IsHardwareAccelerated által jelentett legnagyobb vektorszélességet. Ha ezt a hardver által támogatott szint alá csökkenti, az kikapcsolja a szélesebb szélességeket: egy 512 bites vektorokat támogató gépen a DOTNET_PreferredVectorBitWidth=256 hatására a Vector512<T>.IsHardwareAccelerated a false értéket jelzi. Ez egy általános beállítási lehetőség, de jelenleg csak az x86/x64 támogat 128-nál nagyobb szélességeket, így csak ott van megfigyelhető hatása.

Az x86/x64 utasításkészletek minden logikai csoportosítása saját kapcsolóval is rendelkezik:

Gomb (DOTNET_ előtag) Default Gates
EnableAVX 1 AVX és függők
EnableAVX2 1 AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE és függők
EnableAVX512 1 AVX-512 F+BW+CD+DQ+VL és függők
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 (kiterjesztett általános célú nyilvántartások)
EnableAES 1 AES, PCLMULQDQ
EnableAVX512VP2INTERSECT 1 AVX-512 VP2INTERSECT
EnableAVXIFMA 1 AVX-IFMA
EnableAVXVNNI 1 AVX-VNNI
EnableAVXVNNIINT 1 VEX AVX-VNNI-INT8 és AVX-VNNI-INT16
EnableGFNI 1 GFNI
EnableSHA 1 SHA
EnableVAES 1 VAES, VPCLMULQDQ
EnableWAITPKG 1 WAITPKG
EnableX86Serialize 1 X86 SERIALIZE

Az Arm64-en az utasításkészletek minden logikai csoportosítása saját kapcsolóval rendelkezik:

Gomb (DOTNET_ előtag) Default Gates
EnableArm64Aes 1 AES
EnableArm64Atomics 1 Nagy rendszerbővítmények (LSE) atomi műveletei
EnableArm64Crc32 1 CRC32
EnableArm64Dczva 1 DC ZVA gyorsítótár-nullázás
EnableArm64Dp 1 Pont termék
EnableArm64Rdm 1 Kerekítő duplázó szorzás-összegzés (RDM)
EnableArm64Sha1 1 SHA1
EnableArm64Sha256 1 SHA256
EnableArm64Rcpc 1 Kiadáskonzisztens, processzorkonzisztens rendezés (RCpc)
EnableArm64Rcpc2 1 RCpc2
EnableArm64Cssc 0 Gyakori rövid sorozatú tömörítés (CSSC)
EnableArm64Sve 1 Méretezhető vektorkiterjesztés (SVE)
EnableArm64Sve2 1 SVE2
EnableArm64Sha3 1 SHA3
EnableArm64Sm4 1 SM4
EnableArm64SveAes 1 SVE AES
EnableArm64SveSha3 1 SVE SHA3
EnableArm64SveSm4 1 SVE SM4

Az alapértelmezetten 0 értékre állított kapcsoló (például EnableAVX10v2 vagy EnableArm64Cssc) egy még bevezetés alatt álló utasításkészletet vezérel, ezért az kikapcsolva marad, amíg Ön kifejezetten nem engedélyezi.

Teljesítményteszt a győzelem megerősítéséhez

A vektorizálás növeli a bonyolultságot, ezért mérd meg, hogy megéri-e, mielőtt megtartanád. Használja a BenchmarkDotNet-et, és a korábban bemutatott ugyanazon környezeti változókat használva hasonlítsa össze a skaláris, Vector128 és Vector256 implementációkat egyetlen futtatás során. A BenchmarkDotNet szétszerelési diagnosztizálója a létrehozott szerelvényt is képes kibocsátani, ami felbecsülhetetlen értékű a nagy teljesítményű kód finomhangolásakor.

Néhány dolog, amit szem előtt kell tartani:

  • A nagyobb bemenetek jobban hasznosak. Kis pufferek esetén a vektoros kód lassabb lehet, mint a skaláris kód a beállítási többletterhelés miatt. Mérje fel a hívók által ténylegesen használt bemeneti méreteket.
  • A gyorsítások ritkán tökéletesek. A 32 bites elemeken működő 256 bites vektor nem lesz megbízhatóan 8-szor gyorsabb; ebben a memória-átviteli sebesség, az illesztés és az utasításkésleltetés is szerepet játszik.
  • A memória igazítása befolyásolja a stabilitást. A véletlenszerű kiosztás igazítása zajt okoz az egyes futtatások között. A stabil eredmények érdekében a AlignedAlloc használatával igazított memóriát foglalhat le, vagy engedélyezheti a BenchmarkDotNet memória-randomizálását, hogy megfigyelhesse a teljes eloszlást.

Bevált gyakorlatok

  • Először a meglévő magasabb szintű API-k elérése. Span<T>, stringLINQ TensorPrimitives, és a tenzortípusok már számos gyakori műveletet felgyorsítanak– ne hajtsa végre a már optimalizált és tesztelt műveleteket.
  • Kezdje a Vector128<T> használatával; ez a hardverek legszélesebb körén gyorsított, és nincs szüksége Vector256<T> használatára ahhoz, hogy helyes, hordozható implementációt kapjon. Szélesebb szélességeket és hardveres belső vonalat csak mért gyakori elérésű útvonalakhoz adhat hozzá.
  • A gyorsítótárazásuk helyett közvetlenül ellenőrizze a IsHardwareAccelerated és a Count elemet; a JIT konstansokká alakítja őket.
  • Mindig kezelje a ciklus fennmaradó részét, és a tárolás során figyelembe kell vennie az átfedésben lévő forrás- és célpuffereket.
  • Először írjon él-esetteszteket, majd skaláris megoldást, majd fejezze ki ezt a skaláris logikát a vektor API-kkal.
  • Tesztelje az összes kód elérési útját (beleértve a hozzáférés-megsértéseket is) és mérje fel a reális bemeneti méreteket, mielőtt elkötelezi magát a hozzáadott összetettség mellett.

Lásd még