Použití vnitřních objektů SIMD a hardwaru v .NET

SIMD (jedna instrukce, více dat) je hardwarová podpora pro použití jedné operace na více částí dat paralelně s jedinou instrukcí. Vektorizovaný kód zpracovává v každé iteraci několik hodnot místo jedné, což může výrazně zvýšit propustnost u numerických, vědeckých, grafických, textových a datově paralelních úloh, při nichž se nad bufferem opakuje tatáž operace. Nevýhodou je vyšší složitost, takže se to nejvíc vyplatí tehdy, když je vstup dostatečně velký a přínos je ověřen měřením.

.NET poskytuje několik druhů podpory SIMD. Vyberte ten, který odpovídá tomu, kolik ovládacího prvku potřebujete a kolik složitosti jste ochotni převzít.

Typy podpory SIMD v .NET

API Namespace Kdy ji použít
Jednoúčelové typy vektorů a matic System.Numerics Matematika grafiky a geometrie s vektory prvků 2–4, maticemi, kvaterniony a rovinami.
Vector<T> System.Numerics Přenosná vektorizace s proměnlivou šířkou, pokud nepotřebujete řízení podle platformy.
Vector64<T>, Vector128<T>, , Vector256<T>Vector512<T> System.Runtime.Intrinsics Vektorizace s pevnou šířkou pro různé platformy a s podrobnou kontrolou. Toto je doporučený výchozí bod pro nové vektorizované algoritmy.
Hardwarové vnitřní objekty System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm, System.Runtime.Intrinsics.Wasm Konkrétní instrukce procesoru, které API vyšší úrovně nezpřístupňují, pro dosažení maximálního výkonu v kritické části kódu.
TensorPrimitives System.Numerics.Tensors Předdefinovaná vektorizovaná matematika napříč rozsahy. Provede vektorizaci za vás.

Pokud se tato rozhraní API překrývají, souvisejí prostřednictvím vrstev abstrakce. Obecné vektorové typy jsou základní typy pro výměnu dat, které si ostatní vrstvy předávají, takže jsou z technického hlediska na nejnižší úrovni: proměnně široký typ Vector<T>, který se rozšíří na šířku, kterou podporuje běžící hardware, a pevně široké typy Vector64<T>Vector512<T>. Hardwarové intrinsiky specifické pro platformu v System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm a System.Runtime.Intrinsics.Wasm pracují s těmito typy, přičemž každá z nich přímo odpovídá jednotlivé instrukci procesoru. Operace napříč platformami zpřístupněné na generických typech jsou o úroveň výš než intrinziky specifické pro danou platformu a pro každý cíl se na ně překládají. Ještě výše jsou spravovaná API, která pracují nad celými vyrovnávacími paměťmi – vektorizované metody pro Span<T>, string a TensorPrimitives – a staví na nižších vrstvách, takže získáte akceleraci SIMD, aniž byste cokoli z toho museli psát ručně. Typy System.Numerics s pevným tvarem jsou pomocné typy specifické pro grafiku a geometrii, nikoli součást této vrstvy pro výměnu dat.

Zbytek tohoto článku funguje prostřednictvím těchto rozhraní API od nejvyšší úrovně po nejnižší, pak se zabývá testováním, srovnávacími testy a osvědčenými postupy.

System.Numerics – vektorové a maticové typy

Obor názvů System.Numerics poskytuje typy akcelerované pomocí SIMD s pevně daným tvarem:

Tyto typy se přirozeně hodí pro grafiku a geometrii a běhové prostředí jejich operace urychluje pomocí instrukcí SIMD, pokud je hardware podporuje. Následující příklad přidá dva vektory:

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

Zpřístupňují také běžné operace vektorové matematiky, které byste očekávali, například skalární součin, vzdálenost a omezení hodnoty:

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

Typy matice podporují maticovou matematiku, jako je transponace a násobení:

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> představuje vektor proměnné šířky primitivního číselného typu. Jeho délka je pevná pro dobu životnosti procesu, ale hodnota Vector<T>.Count závisí na procesoru, na kterém se kód spouští. Kompilátor Just-In-Time (JIT) považuje za Count konstantu, takže smyčky napsané proti němu jsou dobře optimalizované.

Vector<T> poskytuje přenosnou vektorizaci bez kódu platformy za cenu, že při kompilaci neznáte šířku vektoru. Následující příklad vypočítá sčítání dvou polí po prvcích:

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

Poznámka:

Tento příklad je ilustrativní. Smyčku, jako je tato, ručně potřebujete napsat jen zřídka, protože TensorPrimitives už poskytuje akcelerované matematické operace založené na rozsahu. Toto sčítání po prvcích je Add a také jsou k dispozici redukční operace, jako je Sum. Tyto operace jsou hardwarově akcelerované pro typy prvků, které Vector<T> podporují (Vector<T>.IsSupported).

Kontrola hardwarové akcelerace

Typy akcelerované pomocí SIMD fungují i na hardwaru nebo v konfiguracích JIT, které SIMD nepodporují, protože využívají neakcelerované softwarové implementace. Pokud chcete zjistit, jestli je akcelerace skutečně dostupná, zkontrolujte příslušnou IsHardwareAccelerated vlastnost:

JIT tyto vlastnosti převede na konstanty, takže nepoužité větve jsou odstraněny a jejich kontrola tedy za běhu nic nestojí. Neukávejte hodnoty do mezipaměti; přečtěte si je přímo tam, kde je potřebujete. Totéž platí pro vlastnosti Count (například Vector128<T>.Count), které jsou také konstantami v době JIT kompilace.

Většina operací nad akcelerovanou šířkou je rovněž akcelerovaná, ale u všech operací to zaručeno není. Například dělení čísel s plovoucí řádovou čárkou může být urychleno, zatímco dělení celých čísel nikoli. Když se akceleruje Vector256, obvykle se akceleruje také Vector128, ale není to zaručeno, proto ověřte každou používanou šířku.

Tip

Pokud operace, kterou potřebujete, není akcelerována na platformě, která je pro vás důležitá, nebo pokud byste uvítali nové multiplatformní rozhraní API, nahlaste problém na dotnet/runtime. Totéž platí pro vylepšení codegenu.

Ne každý typ prvku je platný pro každý vektor. Vector128<T> a související typy aktuálně podporují primitivní číselné typy (byte, sbyte, short, ushort, int, uint, long, ulong, float, double, nint a nuint) a tato sada se může v budoucnu rozšířit o další typy. Slouží Vector128<T>.IsSupported k určení, zda je daná hodnota T platná, což je užitečné zejména z obecného kódu.

Typy, které nejsou podporovány, například char a bool, lze stále vektorizovat tak, že se buffer reinterpretuje na podporovaný typ o stejné velikosti. Pomocí Cast lze reinterpretovat span — například z char na ushort — nebo pomocí metody vektoru As<TFrom, TTo> reinterpretovat vektor, který již máte k dispozici. Reinterpretace změní pouze typ, nikoli podkladové bity, takže je vaší zodpovědností zachovat správně formátovaná data: bool musí zůstat 0 nebo 1a char musí zůstat platnou jednotkou kódu UTF-16. Pokud vektorizovaná operace může vytvořit hodnotu mimo rozsah, před zápisem zpět je potřeba výsledek normalizovat.

Vektorizace napříč platformami s vector128

Vector128<T> je společný jmenovatel pro každou platformu, která podporuje vektorizaci, takže je to nejlepší místo, kde začít. Obsahuje 128bitový vektor: 16 bajtů, 8 shortů, 4 inty/floaty nebo 2 longy/doubly.

------------------------------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> je dvakrát širší a Vector512<T> ještě dvakrát širší. Ne všechen hardware podporuje větší šířky, takže následující příklady používají Vector128 kvůli přenositelnosti.

Pro každou šířku existuje generický typ (Vector128<T>) pro data a negenerická statická třída (Vector128), která obsahuje většinu operací, včetně statických továrních metod, jako jsou Create a Load. Operátory jako +, & a << jsou idiomatickým způsobem, jak vyjádřit aritmetické a bitové operace; upřednostňujte je před pojmenovanými ekvivalenty metod, abyste předešli chybám v prioritě operátorů a zlepšili čitelnost. U algoritmů, které závisí na pořadí bajtů, se rozhodujte podle IsLittleEndian, které JIT také vyhodnotí jako konstantu.

Poznámka:

Na architekturách x86/x64 se operace Vector256<T> obvykle považují za dvě nezávislé 128bitové vektorové cesty. U většiny operací po jednotlivých prvcích je to transparentní, ale operace, které překračují hranice linek (například přeskupování nebo párové/horizontální operace), se mohou chovat odlišně nebo být nákladnější než ekvivalent Vector128. Před tím, než se předpokládá, že širší vektor je rychlejší, potvrďte srovnávací testy.

Provoz pro přecházení jízdních pruhů se zdarma nerozšíří

Operace po prvcích nezávisí na šířce: v1 + v2 představuje stejný výsledek pro každý prvek bez ohledu na to, zda v1 a v2 jsou Vector128<T> nebo Vector256<T> — rozšíření jen zpracuje o jednu datovou dráhu navíc na instrukci. Add nad v = [a, b, c, d] a w = [e, f, g, h] vždy kombinuje prvky se stejným indexem:

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

Operace překračování jízdních pruhů se nedají jednoduše vertikálně navýšit, protože to, které prvky se zkombinují, závisí na šířce vektoru. Párová redukce kombinuje sousední prvky místo prvků se stejným indexem, takže rozšíření změní, které prvky se nakonec spárují:

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)

To je přesně to, co dělá horizontální redukce: ve dvou krocích párového sčítání sečte prvky vektoru. Na architektuře x86/x64 vám k tomu stačí Vector128<float> (4 prvky) a dvě volání 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();
}

Rozšiřte stejný vzorec dvou volání i na Vector256<float> (8 prvků) a vypadá to správně – ale není. HorizontalAdd nefunguje v celém 256bitovém vektoru; opakuje spárovaný vzor nezávisle v každém 128bitovém pruhu. Po dvou krocích získáte součet spodní poloviny (prvky 0–3) rozkopírovaný do celé spodní poloviny a součet horní poloviny (prvky 4–7) rozkopírovaný do celé horní poloviny, nikoli součet všech osmi prvků:

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

Chcete-li získat správný celkový součet, explicitně překleněte hranici mezi pruhy: načtěte dílčí součet každého pruhu pomocí GetLower/GetUpper a sečtěte je dohromady — GetLower a GetUpper rozdělují vektor na první a druhou polovinu:

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

Tento dodatečný krok představuje skutečné náklady na přejíždění jízdních pruhů. Algoritmus pracující napříč datovými pruhy se nedá bez dalších nákladů jednoduše rozšířit tak, jako algoritmus po jednotlivých prvcích — než budete předpokládat, že širší vektor zvítězí, nejdřív to změřte.

Běžné operace

Vector128 a jeho související širší varianty nabízejí rozsáhlé rozhraní API. Nemusíte si je pamatovat – seznamte se s kategoriemi a hledejte podrobnosti, když je potřebujete. Každá operace má softwarovou náhradu pro platformy, které ji nemohou hardwarově akcelerovat. Následující tabulka v podstatě pokrývá celou plochu.

Kategorie Jak funguje Reprezentativní rozhraní API
Constants Předdefinované vektory konstant Zero, One, NegativeOne, AllBitsSet, Indices, SignSequence, E, Pi, Tau, Epsilon, NaN, PositiveInfinity, NegativeInfinity, NegativeZero
Creation Rozšířit skalár, nastavit prvky nebo vygenerovat sekvenci Create, CreateScalar, CreateScalarUnsafe, , Create(ReadOnlySpan<T>)CreateSequence, CreateGeometricSequence, , CreateHarmonicSequenceCreateAlternatingSequence
Načtení a uložení Přesun dat mezi pamětí a vektorem Load, LoadUnsafe, LoadAligned, LoadAlignedNonTemporal, Store, StoreUnsafe, StoreAligned, StoreAlignedNonTemporal, CopyTo, TryCopyTo
Arithmetic Matematika a snížení podle prvků Add (x + y), Subtract (x - y), Multiply (x * y), Divide (x / y), Negate (-x), AddSaturate, SubtractSaturate, Abs, Sqrt, FusedMultiplyAdd, Dot, Sum
Bitové operace Bitová logika a posuny 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 a omezení Omezení minima, maxima a rozsahu po prvcích Min, Max, Clamp, MinMagnitude, MaxMagnitude, MinNumber, MaxNumber, MinMagnitudeNumber, MaxMagnitudeNumber
Zaokrouhlování Zaokrouhlí každý prvek na celočíselnou hodnotu. Ceiling, Floor, , RoundTruncate
Matematické funkce Znaménko, interpolace, úhly a pomocné funkce pro transcendentní funkce CopySign, Lerp, DegreesToRadians, RadiansToDegrees, Hypot, Sin, Cos, SinCos, Asin, Exp, Log, Log2
Porovnání Porovnejte každý prvek; výsledkem je vektorová maska, nikoli to, co vrací operátor bool. Equals, GreaterThan, GreaterThanOrEqual, , LessThanLessThanOrEqual
Classification Predikáty pro jednotlivé prvky na číselné ose, z nichž každý vrací vektorovou masku IsNaN, IsFinite, IsInfinity, IsPositiveInfinity, IsNegativeInfinity, IsInteger, IsEvenInteger, IsOddInteger, IsNegative, IsPositive, IsNormal, IsSubnormal, IsZero
Redukce porovnáním Sbalit porovnání jednotlivých prvků do jednoho bool EqualsAll (x == y), EqualsAny, GreaterThanAll, GreaterThanAny, GreaterThanOrEqualAll, GreaterThanOrEqualAny, LessThanAll, LessThanAny, LessThanOrEqualAll, LessThanOrEqualAny
Predikáty celých vektorů Zredukujte vektor na bool: zda se všechny, některé nebo žádné prvky rovnají hodnotě, nebo zda (ve formách WhereAllBitsSet) mají všechny bity nastavené. Tyto varianty upřednostněte před převedením masky na index. All, Any, None, AllWhereAllBitsSet, , AnyWhereAllBitsSetNoneWhereAllBitsSet
Search Počítat nebo vyhledat prvky podle hodnot, nebo (formy WhereAllBitsSet) nastavit pozice v masce Count, IndexOf, LastIndexOf, CountWhereAllBitsSet, , IndexOfWhereAllBitsSetLastIndexOfWhereAllBitsSet
Maska na index Převést masku porovnání na skalární bitovou masku a naskenovat ji ExtractMostSignificantBits s TrailingZeroCount nebo LeadingZeroCount
Selection Smísit dva vektory podle masky, bit po bitu ConditionalSelect(x, y, z), ekvivalentní k (y & x) \| (z & ~x)
Conversion Změna číselného typu, výpočet nových hodnot (například int na float) ConvertToInt32, ConvertToInt64, ConvertToUInt32, ConvertToUInt64, , ConvertToSingleConvertToDouble
Rozšíření a zúžení Rozdělte prvky na širší typ nebo je zabalte do užšího typu. Widen, WidenLower, WidenUpper, , NarrowNarrowWithSaturation
Reinterpretace Interpretujte bity jako jiný typ prvku, aniž by se změnily As<TFrom, TTo>, AsByte, AsInt32, AsSingle a další podoby prvku As*
Interoperabilita System.Numerics Převod reinterpretací mezi Vector128<T> a numerickými typy s pevným tvarem AsVector, AsVector2, AsVector3, , AsVector4AsPlane, AsQuaternion, , AsVector128AsVector128Unsafe
Změna pořadí Změna uspořádání prvků podle indexu nebo prokládání dvou vektorů Shuffle, Reverse, Zip, ZipLower, ZipUpper, Unzip, UnzipEven, UnzipOdd, ConcatLowerLower, ConcatLowerUpper, ConcatUpperLower, ConcatUpperUpper
Přístup k pruhu Čtení nebo nahrazení jednotlivých prvků a polovin nebo změna velikosti vektoru GetElement, WithElement, ToScalar, , GetLowerGetUpper, WithLower, , WithUpperToVector256

Tip

Několik operací má varianty Estimate a Native — například MultiplyAddEstimate, ClampNative, MinNative, MaxNative, ShuffleNative a ConvertToInt32Native. Odpovídají rychlejší hardwarové instrukci, která je vykoupena nižší přesností nebo se vzdává záruky IEEE pro okrajové případy (například při zpracování NaN), takže po nich sáhněte jen tehdy, když benchmark ukáže, že přesná varianta je úzkým hrdlem a volnější sémantika je přijatelná.

Poznámka:

Vector256.Shuffle zpracovává svůj vstup jako jediný 256bitový vektor, zatímco platformně specifické Avx2.Shuffle pracuje se dvěma nezávislými 128bitovými částmi. Multiplatformní rozhraní API je přenosnější volbou, ale ověřte chování, které potřebujete při přenosu ručně psaných vnitřních objektů.

Strukturování cesty kódu

Vektorizovaná metoda se obvykle větví na cestu pro každou šířku vektoru a na skalární záložní variantu pro malé vstupy a hardware bez akcelerace. Pokud chcete použít největší vektor, který hardware podporuje, nejprve zkontrolujte nejširší vektor a proveďte následující postup:

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

Vnější ochrana každé šířky kombinuje Vector128.IsHardwareAccelerated (konstanta JIT-time pro to, zda platforma tuto šířku zrychluje) s Vector128<T>.IsSupported (zda je typ T prvku platný pro danou šířku). Uvnitř podporovaného bloku porovnejte délku vstupu s hodnotou Count, abyste zvolili mezi vektorizovanou variantou a záložní variantou pro krátký vstup. Metoda je generická pro T a bloky Vector256 a Vector512 – identické s blokem Vector128, ale používající širší typ – jsou pro stručnost zobrazeny jako zakomentované.

Jsou dvě odlišné záložní varianty. Vyrovnávací paměť, která je příliš malá i pro ten nejužší vektor, se však na akcelerovaném hardwaru přesměruje na SumVectorSmall – explicitní tabulku skoků switch, která bez smyčky zpracuje každou možnou délku podvektoru:

// 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) je také konstanta známá v době JIT kompilace, takže SumVectorSmall podle šířky prvku vybírá tabulku dimenzovanou pro počet prvků odpovídající nejširšímu vektoru – stejný přístup, jaký používá i TensorPrimitives. (Pokud je povolen nový model bezpečnosti paměti, sizeof(T) výrazy pro parametr typu s unmanaged omezením jsou povoleny v bezpečném kódu.) Zobrazí se pouze 4 bajtová tabulka; 1 bajtové, 2 bajtové a 8 bajtové tabulky sdílejí svůj tvar. Ve větších případech sloučí zbývající část pomocí Vector256 nebo Vector128 se dvěma překrývajícími se načteními — jedním od začátku a druhým od konce — takže obsluha širšího zbytku pro vynechané cesty Vector512/Vector256 je přímo v tabulce skoků. Obě zatížení se překrývají vždy, když délka není přesným násobkem šířky, takže ocas je před součtem ConditionalSelect maskován na přídatnou identitu. Tato maska je nutná pouze proto, že sčítání není idempotentní; idempotentní operace, jako je například vyhledávání, by mohla překrývající se konec zahrnout přímo. Buffer na hardwaru, který vůbec nepodporuje vektorizaci, přejde na SumScalar, obyčejnou skalární smyčku.

Projděte vstup a zpracujte zbytek

Chcete-li zpracovat vyrovnávací paměť větší než velikost jednoho vektoru, zpracovávejte ji po jednotlivých vektorech a pak zpracujte zbývající prvky, které nezaplní celý vektor. Robustní způsob, jak zpracovat zbylé prvky, spočívá v opětovném zpracování prvků odpovídajících poslednímu plnému vektoru, přičemž se překryjí některé prvky, které už smyčka zpracovala — čímž se zabrání nutnosti samostatné skalární dohry. Zda je třeba toto překrytí opravit, závisí na operaci.

Neidempotentní operace, například součet, by započítala překrývající se prvky dvakrát, takže je před jejich sloučením zamaskujte na neutrální prvek operace. Tuto možnost použijte, když každý prvek musí přispívat přesně jednou:

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

Tato verze podmiňuje rozbalenou smyčku pomocí if, takže malé objemy dat zcela vynechají čtyři akumulátory a přejdou rovnou ke zbytku. Když je k dispozici dostatek dat, do/while v každé iteraci akumuluje čtyři vektory do nezávislých akumulátorů, což procesoru umožňuje pipelinovat operace sčítání, a následně je slučuje po dvojicích. Tabulka skoků pak zahrne zbývající nula až tři celé vektory a v case 0 podvektorovém konci zpracování: znovu použije celý vektor přednačtený z konce bufferu, překrývající prvky již zpracované dříve, a tento překryv zamaskuje na aditivní identitu pomocí ConditionalSelect, takže konec zůstane vektorizovaný, místo aby se přešlo na skalární smyčku. Stejně jako dříve Vector128.Create čte prvky Vector128<T>.Count ze span. JIT u typických vzorců přístupu vynechá kontrolu mezí spanu, takže Create je rozumná výchozí volba i ve výkonnostně kritické smyčce; LoadUnsafe (o tom bude řeč dále) je nízkoúrovňová alternativa pro případ, kdy procházíte buffer pomocí spravovaného odkazu. U velmi velkých vstupů by úplná implementace také zarovnala vyrovnávací paměť a používala netemporální načítání a zápisy, aby se zabránilo vytlačení užitečných řádků mezipaměti — oboje je zde vynecháno a plně pokryto v TensorPrimitives.

Idempotentní operace, jako je například vyhledání hodnoty, může překrytí neškodně zpracovat, takže se poslední vektor přeloží přímo bez masky:

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

Nesprávné zpracování zbytku je častým zdrojem chyb. Smyčka, která čte za konec vyrovnávací paměti, vede k nedeterministickým výsledkům a může způsobit pád. Testovací sada modulu runtime používá pomocnou rutinu BoundedMemory , která umístí stránku bez přístupu hned za vyrovnávací paměť, takže jakékoli čtení mimo hranice vyvolá AccessViolationException během testování. Vždy pokryjte logiku zbytku včetně vyrovnávacích pamětí, jejichž délka není násobkem šířky vektoru.

Bezpečné načítání a ukládání vektorů

Pro většinu kódu jsou Vector128.Create(span) a CopyTo nejjednodušším způsobem, jak přesouvat data mezi spanem a vektorem, a JIT zajišťuje, že zůstávají efektivní. Pokud potřebujete operace načítání a ukládání na nižší úrovni – například pro průchod vyrovnávací pamětí pomocí spravovaného odkazu – upřednostňujte přetížení LoadUnsafe a StoreUnsafe, která přijímají spravovaný odkaz a posun prvku nuint. Na rozdíl od přetížení používajících ukazatele Load/Store nevyžadují fixaci bufferu v paměti a na rozdíl od přímé aritmetiky s odkazy u nich nemusíte ručně posouvat ref. Obě alternativy je snadné pokazit tak, že vzniknou chyby typu garbage-collector holes nebo porušení přístupu.

Aby se prázdné vyrovnávací paměti nevyvolaly, získejte počáteční odkaz ( GetReference nebo GetArrayDataReference pro pole) místo ref span[0].

Important

Aritmetika offsetů používá typy bez znaménka nuint. Vždy zkontrolujte délku vyrovnávací paměti před výpočtem posunu, například buffer.Length - Vector128<int>.Count. Pokud je buffer menší než jeden vektor, při tomto odčítání dojde k podtečení na velmi vysokou hodnotu a smyčka začne číst z neplatné paměti.

Vnitřní funkce hardwaru specifické pro platformu

Pokud vám konkrétní instrukce procesoru poskytuje výhodu, kterou přenosná rozhraní API nenabízejí, sáhněte po hardwarových intrinsic funkcích v System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm a System.Runtime.Intrinsics.Wasm. Každá vnitřní třída má IsSupported vlastnost (také konstantu JIT), takže můžete chránit specializovanou cestu a vrátit se k přenosnému kódu jinde:

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

Předchozí postup ukazuje, jak podle potřeby aktivovat konkrétní větve kódu pro jednotlivé architektury, je to však záměrně jen jednoduchý příklad: tady to ale ve skutečnosti není potřeba. Přenositelný výraz (vector & mask) == Vector128<byte>.Zero už snižuje na optimální instrukce na každé platformě (například ptest na platformě x86/x64), takže provádí stejnou práci jako ručně napsané větve, a to bez složitosti. Po explicitních intrinsic funkcích sahajte jen tehdy, když konkrétní instrukce podává měřitelně lepší výkon než to, co generují přenositelná API.

Hardwarové vnitřní objekty vyžadují samostatnou implementaci pro každou instrukční sadu, takže je považují za optimalizaci měřených horkých cest, nikoli jako výchozí. API Vector128/Vector256 se už na všech platformách překládají do efektivních instrukcí a v praxi promyšlený kód optimalizovaný pro jednotlivé instrukce nepřináší vždy lepší výsledky. Než se zavážete k dodatečné údržbě, ověřte rozdíl pomocí srovnávacího testu.

Matematika vyšší úrovně s TensorPrimitives

Pokud potřebujete provádět vektorizované matematické operace nad rozsahy a nechcete si sami psát smyčky, TensorPrimitives poskytuje širokou sadu numerických operací – aritmetické operace po prvcích, exponenciální funkce a redukce, jako je skalární součin a kosinová podobnost – které už jsou interně vektorizované. Je k dispozici v balíčku NuGet 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);

Pro úlohy AI a numerické výpočty TensorPrimitives často přináší většinu výhod ručně psaného SIMD bez jakékoli související složitosti.

Testování všech cest kódu

Protože vektorizovaná metoda má několik větví kódu, musí testy pokrýt všechny: větev Vector256, větev Vector128 a skalární větev, přičemž u každé musí být použity vstupy jak dostatečně velké, aby přinášely výhodu, tak příliš malé na to, aby se projevila. Velikost vstupu v testech se může lišit, ale hardwarovou akceleraci nemůžete přepnout na úrovni testu. Místo toho ho před zahájením procesu můžete řídit proměnnými prostředí:

  • Nastavte DOTNET_EnableAVX2=0, aby Vector256.IsHardwareAccelerated vracelo false.
  • Nastavte DOTNET_EnableHWIntrinsic=0, aby se intrinsics úplně zakázaly, takže Vector128, Vector64 a Vector<T> všechny hlásí, že není k dispozici žádná akcelerace.

Chcete-li na jediném počítači otestovat všechny cesty, spusťte sadu testů jednou bez přepisujících voleb, jednou s DOTNET_EnableAVX2=0 a jednou s DOTNET_EnableHWIntrinsic=0. Alternativou je testovat na dostatečně rozmanitém hardwaru, aby je to pokrylo.

Instrukční sady konfiguračních uzlů

Kromě těchto dvou běhové prostředí rozpoznává volbu pro každou logickou skupinu instrukčních sad, přičemž každá má předponu DOTNET_. Jeden přepínač může zahrnovat několik souvisejících instrukčních sad – EnableAVX2 například řídí AVX2 spolu s BMI1, BMI2, F16C, FMA, LZCNT a MOVBE. Nastavením knoflíku 0 zakážete celou skupinu a všechno, co je nad ním vrstvené. Nastavení na 1 (což je pro většinu výchozí hodnota) danou skupinu povolí, ale hardware ji musí skutečně podporovat – povolení volby pro instrukci, kterou aktuální CPU nepodporuje, se ignoruje, takže můžete pouze omezit to, co se používá, nikdy však nevynutíte zapnutí nepodporované instrukce a něco si tím nerozbijete. DOTNET_EnableHWIntrinsic=0 je krajní řešení – vypne úplně všechno až na základní vrstvu, takže Vector128, Vector64 a Vector<T> všechny hlásí, že není k dispozici žádná akcelerace, a kód přejde na svou softwarovou cestu.

Important

Jedná se o diagnostické nástroje určené především k testování a validaci – k prověření každé cesty kódu, reprodukci problému specifického pro daný hardware nebo ověření záložního mechanismu. Nejsou určené pro obecné použití ani pro použití v produkčním prostředí a nejsou kontraktem stability. Následující sada je ta, kterou .NET 11 rozpoznává; ve starších vydáních byla k dispozici jiná sada — zejména nastavení baseline a AVX-512 byla překonfigurována — proto si názvy ověřte podle verze modulu runtime, na kterou cílíte.

Tyto nástroje mají také omezení toho, co dosáhnou. Protože řídí rozhodnutí JIT, neovlivňují kód, který už byl předem zkompilován pomocí ReadyToRun nebo Native AOT, a nemusí nutně ovlivňovat ani interní rutiny, které samotné runtime a základní knihovny používají. Berte je jako způsob, jak řídit svůj vlastní JIT-kompilovaný kód, nikoli jako globální vypínač pro instrukční sadu.

Základní přepínač a omezení šířky platí na všech architekturách:

Knoflík (s předponou DOTNET_) Výchozí Účinek
EnableHWIntrinsic 1 Hlavní spínač pro všechny hardwarové vnitřní objekty; 0 vynutí úplnou softwarovou cestu.
MaxVectorTBitWidth výchozí nastavení systému Omezí Vector<T> na maximální šířku v bitech; hodnota menší než 128 znamená výchozí nastavení systému.
PreferredVectorBitWidth výchozí nastavení systému Omezuje maximální šířku vektoru s pevnou šířkou, který hlásí IsHardwareAccelerated, v bitech; hodnota nižší než 128 znamená systémovou výchozí hodnotu.

Výchozí nastavení MaxVectorTBitWidth systému může být užší než hardware plně podporuje, takže Vector<T> se automaticky nezvětší na nejširší dostupný vektor. Například Vector512<T>.IsHardwareAccelerated může být true, zatímco Vector<T> zůstává 256bitové; nastavte DOTNET_MaxVectorTBitWidth=512, aby se Vector<T> používala v širší šířce.

PreferredVectorBitWidth omezuje maximální šířku vektoru hlášenou pomocí IsHardwareAccelerated. Snížení pod úroveň, kterou hardware podporuje, vypne širší šířky: na stroji, který podporuje 512bitové vektory, DOTNET_PreferredVectorBitWidth=256 způsobí, že Vector512<T>.IsHardwareAccelerated bude hlásit false. Je to obecný knoflík, ale pouze x86/x64 nabízí šířky nad 128 dnes, takže je to jediné místo, kde má pozorovatelný efekt.

Každá logická seskupení instrukčních sad x86/x64 má také svůj vlastní přepínač:

Knoflík (s předponou DOTNET_) Výchozí Gates
EnableAVX 1 AVX a závislosti
EnableAVX2 1 AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE a závislé
EnableAVX512 1 AVX-512 F+BW+CD+DQ+VL a závislé
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 (rozšířené obecné registry)
EnableAES 1 AES, PCLMULQDQ
EnableAVX512VP2INTERSECT 1 AVX-512 VP2INTERSECT
EnableAVXIFMA 1 AVX-IFMA
EnableAVXVNNI 1 AVX-VNNI
EnableAVXVNNIINT 1 VEX AVX-VNNI-INT8 a AVX-VNNI-INT16
EnableGFNI 1 GFNI
EnableSHA 1 SHA
EnableVAES 1 VAES, VPCLMULQDQ
EnableWAITPKG 1 WAITPKG
EnableX86Serialize 1 X86 SERIALIZE

V Arm64 má každé logické seskupení sad instrukcí svůj vlastní přepínač:

Knoflík (s předponou DOTNET_) Výchozí Gates
EnableArm64Aes 1 standard AES
EnableArm64Atomics 1 atomické operace Large System Extensions (LSE)
EnableArm64Crc32 1 CRC32
EnableArm64Dczva 1 DC ZVA nulování mezipaměti
EnableArm64Dp 1 Tečkovaný produkt
EnableArm64Rdm 1 Zaokrouhlované zdvojené násobení s akumulací (RDM)
EnableArm64Sha1 1 SHA1
EnableArm64Sha256 1 SHA256
EnableArm64Rcpc 1 Řazení konzistentní vzhledem k verzím, konzistentní vzhledem k procesoru (RCpc)
EnableArm64Rcpc2 1 RCpc2
EnableArm64Cssc 0 Běžná komprese krátkých sekvencí (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

Volba, která má ve výchozím nastavení hodnotu 0 (například EnableAVX10v2 nebo EnableArm64Cssc), řídí přístup k sadě instrukcí, která se teprve zavádí, takže zůstává vypnutá, dokud ji výslovně nepovolíte.

Srovnávací test pro potvrzení výhry

Vektorizace zvyšuje složitost, takže změřte, že se vyplatí, než ji zachováte. Použijte BenchmarkDotNet a použijte stejné proměnné prostředí zobrazené dříve k porovnání skalárních Vector128a Vector256 implementací v jednom spuštění. Diagnostický nástroj pro disassemblaci v BenchmarkDotNetu může také vypsat vygenerovaný assemblerový kód, což je neocenitelné při ladění vysoce výkonného kódu.

Pár věcí, které byste měli mít na paměti:

  • Větší vstupy jsou přínosnější. U malých vyrovnávacích pamětí může být vektorizovaný kód pomalejší než skalární kód kvůli režii při nastavování. Proveďte srovnávací testy velikosti vstupů, které volající skutečně používají.
  • Zrychlení jsou zřídka dokonalá. 256bitový vektor pracující s 32bitovými prvky nebude spolehlivě osmkrát rychlejší; roli hraje propustnost paměti, zarovnání i latence instrukcí.
  • Zarovnání paměti ovlivňuje stabilitu. Náhodné zarovnávání alokace přidává šum mezi jednotlivými spuštěními. Můžete přidělit zarovnanou paměť pomocí AlignedAlloc pro stabilní výsledky nebo povolit randomizaci paměti v BenchmarkDotNet a sledovat celé rozdělení hodnot.

Osvědčené postupy

  • Nejprve sáhněte po existujících API vyšší úrovně. Span<T>, string, LINQ, TensorPrimitives typy tensorů už za vás urychlují mnoho běžných operací – nevytvářejte ručně to, co už je optimalizované a otestované.
  • Začněte s Vector128<T>; je akcelerováno na nejširší škále hardwaru a k získání správné a přenositelné implementace nepotřebujete Vector256<T>. Přidejte širší šířky a hardwarové vnitřní objekty pouze pro měřené horké cesty.
  • Kontrolujte IsHardwareAccelerated a Count přímo místo jejich ukládání do mezipaměti; JIT je převede na konstanty.
  • Vždy zpracujte zbytek smyčky a při ukládání zohledněte překryv zdrojového a cílového bufferu.
  • Nejprve napište testy hraničních případů, pak skalární řešení a pak tuto skalární logiku vyjádřit pomocí vektorových rozhraní API.
  • Než se rozhodnete přijmout tuto dodatečnou složitost, otestujte každý průchod kódem (včetně chyb přístupu) a změřte výkon na realistických velikostech vstupů.

Viz také