.NET'de SIMD ve donanım iç bilgileri kullanma

SIMD (tek yönerge, birden çok veri), tek bir yönergeyle paralel olarak birden çok veri parçasına tek bir işlem uygulamak için donanım desteğidir. Vektörleştirilmiş kod yineleme başına bir değer yerine birkaç değer işler ve bu da aynı işlemin arabellek üzerinde yinelendiği sayısal, bilimsel, grafik, metin işleme ve veri paralel çalışma türü için aktarım hızını büyük ölçüde artırabilir. Bunun bedeli ek karmaşıklıktır; bu yüzden en çok, girdi yeterince büyük olduğunda ve elde edilen kazanç ölçümlerle doğrulandığında karşılığını verir.

.NET çeşitli SIMD desteği sağlar. İhtiyacınız olan kontrol düzeyine ve üstlenmeye hazır olduğunuz karmaşıklık düzeyine uyan seçeneği seçin.

.NET'de SIMD desteği türleri

API Namespace Ne zaman kullanılır?
Sabit amaçlı vektör ve matris türleri System.Numerics 2-4 öğe vektörleri, matrisler, quaternions ve düzlemlerle grafik ve geometri matematiği.
Vector<T> System.Numerics Platform başına denetime ihtiyacınız olmadığında taşınabilir, değişken genişlikli vektörleştirme.
Vector64<T>, Vector128<T>, Vector256<T>, Vector512<T> System.Runtime.Intrinsics Hassas denetime sahip platformlar arası, sabit genişlikli vektörleştirme. Bu, yeni vektörleştirilmiş algoritmalar için önerilen başlangıç noktasıdır.
Donanım iç bilgileri System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm, System.Runtime.Intrinsics.Wasm Üst düzey API'lerin sunmadığı, kritik bir kod yolunda performansın son kısmını elde etmek için kullanılan belirli işlemci yönergeleri.
TensorPrimitives System.Numerics.Tensors Aralıklar üzerinde hazır, vektörleştirilmiş matematik. Vektörleştirmeyi sizin için yapar.

Bu API'lerin çakıştığı yerlerde soyutlama katmanlarıyla ilişkilidir. Genel vektör türleri, diğer katmanların aralarında aktardığı temel veri alışverişi türleridir; bu nedenle teknik olarak en alt düzeydedirler: çalışan donanımın desteklediği genişliğe kadar büyüyebilen değişken genişlikli Vector<T> ve sabit genişlikli Vector64<T> ile Vector512<T>. System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm ve System.Runtime.Intrinsics.Wasm içindeki platforma özgü donanım intrinsic’leri bu türlerle çalışır; her biri doğrudan tek bir işlemci yönergesine karşılık gelir. Genel türlerde sunulan platformlar arası işlemler, platforma özgü içsel işlevlerin bir adım üzerinde yer alır ve her hedef için onlara indirgenir. Bunun da üstünde, tüm tamponlar üzerinde çalışan yönetilen API'ler yer alır: Span<T> ve string üzerindeki vektörleştirilmiş yöntemler ile TensorPrimitives; bunlar alttaki katmanların üzerine inşa edildiğinden, bunların hiçbirini elle yazmanıza gerek kalmadan SIMD hızlandırması elde edersiniz. System.Numerics Sabit şekilli türler, bu değişim yığınının bir parçası olmaktan ziyade, grafikler ve geometri için alana özgü kullanışlı türlerdir.

Bu makalenin geri kalanı bu API'ler aracılığıyla en üst düzeyden en alt düzeye kadar çalışır, ardından test, karşılaştırma ve en iyi yöntemleri kapsar.

System.Numerics vektör ve matris türleri

System.Numerics ad alanı, SIMD hızlandırmalı sabit şekilli türler sağlar:

Bu türler grafik ve geometriye doğal olarak karşılık gelir ve çalışma zamanı, donanım desteklediğinde, işlemlerini SIMD yönergeleriyle hızlandırır. Aşağıdaki örnek iki vektör ekler:

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

Ayrıca nokta çarpımı, mesafe ve sınırlandırma gibi beklediğiniz yaygın vektör matematiği işlemlerini de sunarlar:

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

Matris türleri, dönüştürme ve çarpma gibi matris matematiği destekler:

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

Vektör<T>

Vector<T> , ilkel sayısal türün değişken genişlikli vektörlerini temsil eder. Uzunluğu işlemin ömrü için sabittir, ancak değeri Vector<T>.Count kodu çalıştıran CPU'ya bağlıdır. Just-In-Time (JIT) derleyicisi Count öğesini sabit olarak ele alır; bu nedenle buna göre yazılan döngüler iyi optimize edilir.

Vector<T> , derleme zamanında vektör genişliğini bilmemeniz karşılığında platform başına kod olmadan taşınabilir vektörleştirme sağlar. Aşağıdaki örnek, iki dizinin öğeye göre eklenmesini hesaplar:

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

Uyarı

Bu örnek açıklayıcıdır. TensorPrimitives zaten hızlandırılmış span tabanlı matematik sağladığından, böyle bir döngüyü elle yazmanız nadiren gerekir. Bu öğe bazında toplama işlemi Add şeklindedir ve Sum gibi indirgeme işlemleri de sunulur. Bu işlemler, Vector<T>’ın desteklediği öğe türleri için (Vector<T>.IsSupported) donanım hızlandırmalıdır.

Donanım hızlandırmasını denetleyin

SIMD hızlandırılmış türler, hızlandırılmamış yazılım uygulamalarına geri döndükleri için SIMD'yi desteklemeyen donanım veya JIT yapılandırmalarında bile çalışır. Hızlandırmanın gerçekten kullanılabilir olup olmadığını denetlemek için ilgili IsHardwareAccelerated özelliği denetleyin:

Bu özellikler JIT tarafından sabitlere dönüştürülür, bu nedenle almadığınız dallar ortadan kalkar ve bunları denetlemenin çalışma zamanı maliyeti yoktur. Değerleri önbelleğe alma; bunları doğrudan ihtiyacınız olan yerde okuyun. Aynı durum, JIT zamanı sabitleri olan özellikler (örneğin, Count) için Vector128<T>.Count de geçerlidir.

Hızlandırılmış bir genişlik üzerindeki işlemlerin çoğu kendileri de hızlandırılmıştır, ancak bu durum her işlem için garanti edilmez. Örneğin, tamsayı bölmenin olmadığı durumlarda kayan nokta bölme hızlandırılabilir. Vector256 hızlandırıldığında, Vector128 de genellikle hızlandırılır, ancak bunun garantisi yoktur; bu nedenle kullandığınız her genişliği kontrol edin.

Tip

İlgilendiğiniz bir platformda ihtiyacınız olan bir işlem hızlandırılmıyorsa veya yeni bir platformlar arası API istiyorsanız dotnet/runtime üzerinde bir sorun oluşturun. Aynı durum codegen geliştirmeleri için de geçerlidir.

Her öğe türü her vektör için geçerli değildir. Vector128<T>ve eşdüzeyleri bugün ilkel sayısal türleri (byte, sbyte, short, ushort, int, uint, long, ulong, floatdouble, nintve nuint) destekler ve bu küme gelecekte diğer türleri içerecek şekilde büyüyebilir. Belirli bir T öğesinin geçerli olup olmadığını belirlemek için Vector128<T>.IsSupported kullanın; bu, özellikle genel kodda kullanışlıdır.

char ve bool gibi desteklenmeyen türler, arabellek aynı boyuttaki desteklenen bir tür olarak yeniden yorumlanarak yine de vektörleştirilebilir. Bir aralığı yeniden yorumlamak için Cast kullanın; örneğin, char ile ushort arasında veya zaten sahip olduğunuz bir vektörü yeniden yorumlamak için vektörün As<TFrom, TTo> yöntemini kullanın. Yeniden yorumlama yalnızca türü değiştirir, altta yatan bitleri değil; bu nedenle verilerin doğru biçimlendirilmiş olarak kalmasını sağlamak sizin sorumluluğunuzdadır: bir bool, 0 veya 1 olarak kalmalı ve bir char, geçerli bir UTF-16 kod birimi olarak kalmalıdır. Vektörleştirilmiş bir işlem aralık dışı bir değer üretebiliyorsa, sonucu geri yazmadan önce normalleştirmeye dikkat edin.

Vector128 ile platformlar arası vektörleştirme

Vector128<T> vektörleştirmeyi destekleyen her platformda ortak paydadır, bu nedenle başlamak için en iyi yerdir. 128 bit vektör içerir: 16 bayt, 8 kısa, 4 int/float veya 2 uzun/çift.

------------------------------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> iki kat daha geniştir ve Vector512<T> iki kez daha geniştir. Tüm donanımlar daha geniş genişlikleri desteklemediğinden, taşınabilirlik için aşağıdaki örnekler kullanılır Vector128 .

Her genişliğin, veriler için bir genel türü (Vector128<T>) ve Create ile Load gibi statik fabrika yöntemleri de dahil olmak üzere işlemlerin çoğunu barındıran, genel olmayan bir statik sınıfı (Vector128) vardır. +, & ve << gibi işleçler, aritmetik ve bit işlemlerini ifade etmenin yerleşik yoludur; işleç önceliği hatalarından kaçınmak ve okunabilirliği artırmak için, adlandırılmış yöntem karşılıkları yerine bunları tercih edin. Bayt sırasına bağlı algoritmalar için, JIT’in de sabite indirgediği IsLittleEndian’a göre dallanın.

Uyarı

x86/x64'te Vector256<T> işlemler genellikle iki bağımsız 128 bitlik şerit gibi ele alınır. Çoğu öğe düzeyindeki işlem için bu fark edilmez, ancak şeritler arası işlemler (karıştırmalar veya çiftli/yatay işlemler gibi) farklı davranabilir ya da Vector128 eşdeğerinden daha maliyetli olabilir. Daha geniş bir vektör daha hızlı olduğunu varsaymadan önce karşılaştırmalarla onaylayın.

Şerit geçiş işlemleri ücretsiz olarak genişletmiyor

Öğe bazlı işlemler genişliği dikkate almaz: v1 + v2, v1 ve v2Vector128<T> ya da Vector256<T> olsa da öğe başına aynı sonucu verir; genişletme yalnızca komut başına bir şeritlik daha veriyi işler. Add, v = [a, b, c, d] ve w = [e, f, g, h] üzerinde her zaman aynı indeksteki öğeleri birleştirir:

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

Şeritler arası geçiş işlemleri o kadar kolay ölçeklenmez, çünkü hangi öğelerin birleştirileceği vektörün genişliğine bağlıdır. Çift yönlü azaltma, aynı dizinli öğeler yerine bitişik öğeleri birleştirir; bu nedenle genişletme işlemi, eşleştirilmiş olan öğeleri değiştirir:

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)

Yatay azaltma tam olarak bunu yapar: çift yönlü eklemelerin iki turuyla vektör öğelerini toplama. x86/x64'te Vector128<float> (4 öğe) iki çağrıyla HorizontalAddsizi oraya alır:

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

Aynı iki çağrılı kalıbı Vector256<float>’ye (8 öğe) genişlettiğinizde doğru görünüyor ama öyle değil. HorizontalAdd 256 bit vektör genelinde çalışmaz; her 128 bit şeritte çift yönlü deseni bağımsız olarak yineler. İki adım sonunda, alt şeridin toplamı (0-3. öğeler) alt şeridin tamamına, üst şeridin toplamı da (4-7. öğeler) üst şeridin tamamına yayılır; elde edilen sonuç, sekiz öğenin tümünün toplamı değildir:

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

Doğru toplamı elde etmek için şerit sınırını açıkça köprüleyin: her bir şeridin kısmi toplamını GetLower/GetUpper okuyup bunları bir araya getirin veGetLowerGetUpper bir vektörün ilk ve ikinci yarısına bölün:

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

Bu ek adım, şeritleri geçmenin gerçek maliyetidir. Şerit geçişli bir algoritma, öğe bazlı olanın yaptığı gibi kendiliğinden ek maliyet olmadan genişlemez; daha geniş vektörün kazanacağını varsaymadan önce ölçün.

Yaygın işlemler

Vector128 ve aynı ailedeki daha kapsamlı sürümleri geniş bir API sunar. Ezberlemeniz gerekmez; kategorileri bilin ve ihtiyacınız olduğunda ayrıntıları arayın. Her işlem için, bunu hızlandıramayan platformlarda kullanılacak bir yazılım yedeği vardır. Aşağıdaki tablo temelde tüm yüzeyi kapsar.

Category Ne işe yarıyor? Temsilci API'leri
Constants Önceden tanımlanmış sabit vektörler Zero, One, NegativeOne, AllBitsSet, Indices, SignSequence, E, Pi, Tau, Epsilon, NaN, PositiveInfinity, NegativeInfinity, NegativeZero
Oluşturma Skaler yayınlama, öğeleri ayarlama veya dizi oluşturma Create, CreateScalar, CreateScalarUnsafe, , Create(ReadOnlySpan<T>), CreateSequence, CreateGeometricSequence, CreateHarmonicSequence, CreateAlternatingSequence
Yükleme ve depolama Verileri bellek ve vektör arasında taşıma Load, LoadUnsafe, LoadAligned, LoadAlignedNonTemporal, Store, StoreUnsafe, StoreAligned, StoreAlignedNonTemporal, CopyTo, TryCopyTo
Arithmetic Öğe açısından matematik ve azaltmalar Add (x + y), Subtract (x - y), Multiply (x * y), Divide (x / y), Negate (-x), AddSaturate, SubtractSaturate, Abs, Sqrt, FusedMultiplyAdd, Dot, Sum
Bit işlemleri Bit düzeyinde mantık ve kaydırmalar 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 ve clamp Öğe bazında minimum, maksimum ve aralık sınırlandırma Min, Max, Clamp, , MinMagnitude, MaxMagnitude, MinNumber, MaxNumber, MinMagnitudeNumber, MaxMagnitudeNumber
Rounding Her öğeyi tam sayı değerine yuvarlama Ceiling, Floor, Round, Truncate
Matematik işlevleri İşaret, ilişkilendirme, açı ve transandantal yardımcılar CopySign, Lerp, DegreesToRadians, RadiansToDegrees, Hypot, Sin, Cos, SinCos, Asin, Exp, Log, Log2
Karşılaştırma Her bir öğeyi karşılaştırın; sonuç, bir işlecin verdiği değil, bool bir vektör maskesidir. Equals, GreaterThan, GreaterThanOrEqual, LessThan, , LessThanOrEqual
Classification Sayı doğrusu üzerinde, her biri bir vektör maskesi döndüren öğe başına yüklemler IsNaN, IsFinite, IsInfinity, IsPositiveInfinity, IsNegativeInfinity, IsInteger, IsEvenInteger, IsOddInteger, IsNegative, IsPositive, IsNormal, IsSubnormal, IsZero
Karşılaştırma indirgemeleri Öğe başına karşılaştırmayı tek bir öğeyle daraltma bool EqualsAll (x == y), EqualsAny, GreaterThanAll, GreaterThanAny, GreaterThanOrEqualAll, GreaterThanOrEqualAny, LessThanAll, LessThanAny, LessThanOrEqualAll, LessThanOrEqualAny
Tam vektör yüklemleri Bir vektörü bool değerine indirgeyin: öğelerin tümünün, herhangi birinin veya hiçbirinin bir değere eşit olup olmadığına ya da (WhereAllBitsSet biçimlerinde) tüm bitlerin ayarlanmış olup olmadığına bakın. Maskeyi dizine dönüştürmek yerine bunları tercih edin All, Any, None, , AllWhereAllBitsSet, AnyWhereAllBitsSet, NoneWhereAllBitsSet
Search Öğeleri değere göre sayın veya bulun ya da (WhereAllBitsSet biçimleri) bir maskede şeritleri ayarlayın Count, IndexOf, LastIndexOf, , CountWhereAllBitsSet, IndexOfWhereAllBitsSet, LastIndexOfWhereAllBitsSet
Dizine maskele Karşılaştırma maskesini skaler bir bit maskesine dönüştürmek ve taramak ExtractMostSignificantBits ile TrailingZeroCount veya LeadingZeroCount
Seleksiyon Maskeye göre iki vektörü bit bit harmanlayın ConditionalSelect(x, y, z), şunun eşdeğeridir: (y & x) \| (z & ~x)
Conversion Sayısal türü değiştirme, yeni değerleri hesaplama (örneğin, intfloatolarak) ConvertToInt32, ConvertToInt64, ConvertToUInt32, , ConvertToUInt64, ConvertToSingle, ConvertToDouble
Genişletme ve daraltma Öğeleri daha geniş bir türe bölme veya daha dar bir türe paketleme Widen, WidenLower, WidenUpper, Narrow, , NarrowWithSaturation
Yeniden yorumlama Bitleri değiştirmeden başka bir öğe türü olarak yeniden yorumla As<TFrom, TTo>, AsByte, AsInt32, AsSingleve diğer As* öğe formları
System.Numerics birlikte çalışabilirlik Vector128<T> ile sabit biçimli sayısal türler arasında yeniden yorumlama AsVector, AsVector2, AsVector3, , AsVector4, AsPlane, AsQuaternion, AsVector128, AsVector128Unsafe
Yeniden sipariş ver Öğeleri indekse göre yeniden düzenleme veya iki vektörü iç içe geçirme Shuffle, Reverse, Zip, ZipLower, ZipUpper, Unzip, UnzipEven, UnzipOdd, ConcatLowerLower, ConcatLowerUpper, ConcatUpperLower, ConcatUpperUpper
Şerit erişimi Öğeleri ve yarıları tek tek okuma veya değiştirme ya da vektörün boyutunu değiştirme GetElement, WithElement, ToScalar, , GetLower, GetUpper, WithLower, WithUpper, ToVector256

Tip

Bazı işlemlerin Estimate ve Native varyantları vardır; örneğin MultiplyAddEstimate, ClampNative, MinNative, MaxNative, ShuffleNative ve ConvertToInt32Native. Bunlar, bir miktar kesinlikten ödün veren veya IEEE’nin uç durum garantilerinden bazılarından (örneğin NaN işleme) feragat eden daha hızlı bir donanım komutuna eşlenir; bu nedenle bunlara yalnızca bir performans testi tam biçimin darboğaz olduğunu gösterdiğinde ve daha gevşek semantiğin kabul edilebilir olduğu durumlarda başvurun.

Uyarı

Vector256.Shuffle, girişini tek bir 256 bitlik vektör olarak ele alırken, platforma özgü Avx2.Shuffle ise iki bağımsız 128 bitlik şerit olarak çalışır. Platformlar arası API daha taşınabilir bir seçenektir, ancak elle yazılmış iç bilgileri taşıma sırasında ihtiyacınız olan davranışı onaylayın.

Kod yolunu yapılandırma

Vektörleştirilmiş bir yöntem genellikle vektör genişliği başına bir yola ek olarak küçük girişler ve hızlandırılmamış donanımlar için skaler geri dönüşe doğru ilerler. Donanımın desteklediği en büyük vektöri kullanmak için önce en geniş vektöre bakın ve aşağıdan aşağıya doğru çalışın:

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

Her genişliğin dış koruması, Vector128.IsHardwareAccelerated (platformun bu genişliği hızlandırıp hızlandırmadığını belirleyen JIT derleme zamanı sabiti) ile Vector128<T>.IsSupported (T öğe türünün bu genişlik için geçerli olup olmadığı) öğesini birleştirir. Desteklenen bir bloğun içinde giriş uzunluğunu Count ile karşılaştırarak vektörleştirilmiş yol ile küçük girişli bir geri dönüş arasında seçim yapın. Yöntem, T için geneldir ve Vector256 ile Vector512 blokları—Vector128 bloğuyla aynı olup yalnızca daha geniş türü kullanmaları bakımından farklıdır—kısalık sağlamak amacıyla yorum satırı hâlinde gösterilmiştir.

İki ayrı yedek çözüm vardır. En kısa vektör için bile fazla küçük olan bir arabellek, hızlandırılmış donanımda SumVectorSmall'a gider—döngü kullanmadan mümkün olan her alt vektör uzunluğunu işleyen, açıkça tanımlanmış bir switch atlama tablosuna:

// 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) aynı zamanda JIT derleme zamanında sabittir; bu nedenle SumVectorSmall, öğe genişliğine göre, en geniş vektörün alabileceği sayıda öğeye göre boyutlandırılmış bir tabloya yönlendirme yapar; bu, TensorPrimitives'nin kullandığı yaklaşımın aynısıdır. (Yeni bellek güvenliği modeli etkinleştirildiğinde, sizeof(T) güvenli kodda kısıtlaması olan bir tür parametresindeki unmanaged ifadelere izin verilir.) Yalnızca 4 baytlık tablo gösterilir; 1 bayt, 2 bayt ve 8 baytlık tablolar şeklini paylaşır. Daha büyük durumları, artanı Vector256 veya Vector128 ile, biri baştan diğeri sondan yapılan iki çakışan yükleme kullanarak ele alır; böylece atlanan Vector512/Vector256 yollarına yönelik daha geniş kalan bölüm işleme mantığı doğrudan atlama tablosunda yer alır. Uzunluk genişliğin tam katı olmadığında iki yükleme çakışır; bu nedenle kuyruk, toplanmadan önce ConditionalSelect ile toplamsal özdeş elemana maskelenir. Bu maske yalnızca toplama işlemi idempotent olmadığı için gereklidir; arama gibi idempotent bir işlem, örtüşen kuyruğu doğrudan içine katabilir. Hiçbir vektörleştirmesi olmayan donanım üzerindeki bir arabellek, sıradan bir skaler döngüye SumScalargeçer.

Girdiyi yineleyin ve kalan kısmı işleyin

Tek bir vektörden daha büyük bir arabelleği işlemek için, bir kerede bir vektör üzerinde döngü yapın ve ardından tam vektörü doldurmamış kalan öğeleri işleyin. Bu kuyruğu ele almanın sağlam yolu, son tam vektörlük öğe grubunu yeniden işlemek, yani döngünün zaten işlediği bazı öğelerle çakışmaktır; böylece ayrı bir skaler epilogdan kaçınılır. Bu çakışmanın düzeltilmesi gerekip gerekmediği işleme bağlıdır.

Toplam gibi tek bir kez etkili olmayan bir işlem çakışan öğeleri iki kez sayar, bu nedenle bunları katlamadan önce işlemin kimliğine göre maskeleyin. Her öğenin tam olarak bir kez katkıda bulunması gerektiğinde bunu kullanın:

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

Bu sürüm, açılmış döngüyü bir if ile koruma altına alır; böylece küçük veri yükleri dört akümülatörü tamamen atlayıp doğrudan kalan kısma geçer. Yeterli veri olduğunda, bir do/while, her yinelemede dört vektörü bağımsız biriktiricilerde biriktirir — bu da işlemcinin toplama işlemlerini işlem hattında yürütmesine olanak tanır — ve ardından bunları ikili olarak birleştirir. Bir switch sıçrama tablosu daha sonra kalan sıfır ila üç tam vektörü ve case 0 içindeki alt vektör kuyruğunu hesaba katar: arabelleğin sonundan önceden yüklenmiş tam bir vektörü yeniden kullanır, bu vektör önceden işlenmiş öğelerle çakışır; ardından bu çakışmayı ConditionalSelect ile toplamsal özdeşliğe maskeleyerek kuyruğun skaler bir döngüye düşmek yerine vektörleştirilmiş kalmasını sağlar. Daha önce olduğu gibi, Vector128.Create, span'dan Vector128<T>.Count öğelerini okur. JIT, tipik erişim düzenlerinde span'in sınır denetimini ortadan kaldırır; bu nedenle Create, yoğun çalışan bir döngüde bile iyi bir varsayılan seçenektir; LoadUnsafe ise (bir sonraki bölümde ele alınan), arabelleği yönetilen referansla dolaştığınız durumlar için daha alt düzeyli bir alternatiftir. Çok büyük girdiler için, tam bir uygulama ayrıca arabelleği hizalar ve yararlı önbellek satırlarının önbellekten atılmasını önlemek için zamansal olmayan yükleme ve depolama işlemleri kullanır; bu iki konu burada atlanmış olup TensorPrimitives içinde ayrıntılı olarak ele alınmaktadır.

İdempotent bir değer arama işlemi gibi bir işlem, çakışan kısmı zararsız biçimde yeniden işleyebilir; bu nedenle son vektörü maskesiz olarak doğrudan dahil eder:

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

Geri kalanın yanlış işlenmesi yaygın bir hata kaynağıdır. Arabelleğin sonunun ötesini okuyan bir döngü öngörülemeyen sonuçlar üretir ve çökmeye neden olabilir. Çalışma zamanının test takımı, arabelleğin hemen arkasına erişime kapalı bir sayfa yerleştiren bir BoundedMemory yardımcısı kullanır; böylece arabellek sınırları dışındaki herhangi bir okuma, test sırasında bir AccessViolationException hatasına neden olur. Uzunluğu vektör genişliğinin katı olmayan arabellekler de dahil olmak üzere geri kalan mantığı her zaman kapsar.

Vektörleri güvenli bir şekilde yükleme ve depolama

Çoğu kod için, Vector128.Create(span) ve CopyTo, bir span ile bir vektör arasında veri taşımanın en basit yoludur ve JIT bunların verimliliğini korur. Daha düşük düzeyli okuma ve yazma işlemlerine ihtiyaç duyduğunuzda — örneğin, bir arabellekte yönetilen başvuruyla ilerlemek için — yönetilen bir başvuru ve bir nuint öğe ofseti alan LoadUnsafe ve StoreUnsafe aşırı yüklemelerini tercih edin. İşaretçi tabanlı Load/Store yüklemelerinin aksine, arabelleği sabitlemeniz gerekmez; ham başvuru aritmetiğinin aksine ise ref değerini el ile ilerletmeniz de gerekmez. Her iki alternatif de, çöp toplayıcı açıklarına veya erişim ihlallerine yol açacak şekilde kolayca hatalı kullanılabilir.

Boş arabelleklerin hata fırlatmaması için, başlangıç başvurusunu ref span[0] yerine GetReference'dan alın (veya diziler için GetArrayDataReference'den).

Important

Ofset aritmetiği işaretsiz nuint kullanır. gibi buffer.Length - Vector128<int>.Countbir uzaklığı hesaplamadan önce her zaman arabellek uzunluğunu denetleyin. Arabellek bir vektörden küçükse, bu çıkarma işlemi alt taşmaya uğrayarak çok büyük bir değer üretir ve döngü geçersiz bellekten okuma yapar.

Platforma özgü donanım iç bilgileri

Taşınabilir API'lerin sunmadığı bir avantajı belirli bir işlemci yönergesi sağladığında, System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm ve System.Runtime.Intrinsics.Wasm içindeki donanım intrinsiklerine başvurun. Her iç sınıfın bir IsSupported özelliği (ayrıca bir JIT sabiti) vardır, böylece özel yolu koruyup başka bir yerde taşınabilir koda geri dönebilirsiniz:

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

Yukarıdaki yöntem, mimari başına kod yollarını istediğiniz zaman nasıl aydınlatabileceğinizi gösterir, ancak bu özellikle basit bir örnektir: burada buna gerçekten ihtiyacınız yoktur. Taşınabilir ifade (vector & mask) == Vector128<byte>.Zero zaten her platformda (örneğin, ptest x86/x64 üzerinde) en uygun yönergeye iner, bu nedenle yalnızca karmaşıklık olmadan el ile yazılan dallarla aynı işi yapar. Yalnızca belirli bir komutun, taşınabilir API'lerin ürettiklerinden ölçülebilir biçimde daha iyi performans gösterdiği durumlarda doğrudan intrinsic'lere başvurun.

Donanım intrinsikleri, her komut kümesi için ayrı bir gerçekleştirim gerektirir; bu nedenle bunları varsayılan olarak değil, ölçümlerle belirlenmiş kritik kod yolları için bir optimizasyon olarak değerlendirin. API'ler Vector128/Vector256 zaten her platformda verimli yönergelere indirilir ve pratikte gelişmiş yönerge başına kod her zaman kazanamaz. Ek bakım gerçekleştirmeden önce bir karşılaştırma ile farkı onaylayın.

TensorPrimitives ile daha üst düzey matematik

Span'ler üzerinde vektörleştirilmiş matematik işlemlerine ihtiyacınız varsa ve döngüleri kendiniz yazmak istemiyorsanız, TensorPrimitives, öğe bazında aritmetik, üs alma işlemleri ve nokta çarpımı ile kosinüs benzerliği gibi indirgeme işlemleri dahil olmak üzere, dahili olarak zaten vektörleştirilmiş çok sayıda sayısal işlem sunar. System.Numerics.Tensors NuGet paketinde kullanılabilir.

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

Yapay zeka ve sayısal iş yükleri için TensorPrimitives, çoğu zaman elle yazılmış SIMD’nin sağladığı avantajların büyük kısmını, bunun getirdiği karmaşıklığın hiçbirini içermeden sunar.

Tüm kod yollarını test edin

Vektörleştirilmiş bir yöntemin birkaç kod yolu olduğundan, testler bunların her birini kapsamalıdır: Vector256 yolu, Vector128 yolu ve skaler yol; her biri için hem avantaj sağlayacak kadar büyük hem de fayda sağlamayacak kadar küçük girdilerle. Testlerde giriş boyutunu değiştirebilirsiniz, ancak donanım hızlandırmayı test düzeyinde değiştiremezsiniz. Bunun yerine, işlem başlamadan önce ortam değişkenleriyle denetleyin:

  • Vector256.IsHardwareAccelerated öğesinin false döndürmesi için DOTNET_EnableAVX2=0 değerini ayarlayın.
  • DOTNET_EnableHWIntrinsic=0 değerini yerleşik işlevleri tamamen devre dışı bırakacak şekilde ayarlayın; böylece Vector128, Vector64 ve Vector<T> hiçbir hızlandırma olmadığını bildirir.

Tek bir makinede her yolu çalıştırmak için test kümesini bir kez herhangi bir geçersiz kılma olmadan, bir kez DOTNET_EnableAVX2=0 ile ve bir kez DOTNET_EnableHWIntrinsic=0 ile çalıştırın. Alternatif, bunları kapsayacak kadar çeşitli donanım üzerinde çalışmaktır.

Yönerge kümesi yapılandırma düğümleri

Bu ikisinin ötesinde, çalışma zamanı yönerge kümelerinin her mantıksal gruplandırması için, her biri DOTNET_ ile öneklenen bir ayarı tanır. Tek bir düğme, BMI1, BMI2, F16C, FMA, LZCNT ve MOVBE ile birlikte AVX2 geçitleri gibi çeşitli ilgili yönerge kümeleriniEnableAVX2 kapsar. Bir düğmenin 0 olarak ayarlanması, ait olduğu grubun tamamını ve üzerinde katmanlanan her şeyi devre dışı bırakır. Bunu 1 olarak ayarlamak (çoğu kullanıcı için varsayılan olan) grubun kullanılmasına izin verir, ancak donanımın bunu gerçekten desteklemesi yine de gerekir; mevcut CPU’nun desteklemediği bir seçeneği etkinleştirmek yok sayılır, bu nedenle yalnızca kullanılanları daraltabilirsiniz; desteklenmeyen bir yönergeyi zorla etkinleştirip sistemi bozmanız söz konusu değildir. DOTNET_EnableHWIntrinsic=0 büyük balyozdur; en temel düzeye kadar her şeyi devre dışı bırakır, bu nedenle Vector128, Vector64 ve Vector<T> hiç hızlandırma olmadığını bildirir ve kod yazılım yoluna geçer.

Important

Bunlar, esas olarak test ve doğrulama için tasarlanmış tanılama araçlarıdır; her bir kod yolunu çalıştırmak, donanıma özgü bir sorunu yeniden üretmek veya yedek mekanizmanın devreye girdiğini doğrulamak için kullanılır. Bunlar genel veya üretim amaçlı kullanım için tasarlanmamıştır ve bir kararlılık sözleşmesi değildir. Aşağıdaki küme, .NET 11'in tanıdığı kümedir; önceki sürümlerde farklı bir küme ortaya çıkarılmıştır; özellikle temel ve AVX-512 düğümleri yeniden yapılandırılmıştır; bu nedenle adları hedeflediğiniz çalışma zamanı sürümüne göre onaylayın.

Bu araçların ulaşabilecekleri sınırlar da vardır. JIT kararlarını belirledikleri için, ReadyToRun veya Native AOT aracılığıyla önceden derlenmiş kodu etkilemezler; ayrıca çalışma zamanı ile çekirdek kitaplıkların kendi kullandıkları iç yordamları da her zaman etkilemezler. Bunları, bir yönerge kümesi için genel bir kapatma anahtarı olarak değil, JIT ile derlenmiş kendi kodunuzu yönlendirmenin bir yolu olarak görün.

Temel seçenek ve genişlik sınırları her mimari için geçerlidir:

Düğme (DOTNET_ ön ek) Varsayılan Etkisi
EnableHWIntrinsic 1 Tüm donanım iç bilgileri için ana anahtar; 0 tam yazılım yolunu zorlar.
MaxVectorTBitWidth sistem varsayılanı Vector<T> değerini bit cinsinden azami genişlikle sınırlar; 128'in altındaki bir değer, sistem varsayılanının kullanılacağı anlamına gelir.
PreferredVectorBitWidth sistem varsayılanı IsHardwareAccelerated bildiren sabit genişlikli vektörün bit cinsinden en büyük değerinin üst sınırını belirler; 128'in altındaki bir değer, sistem varsayılanının kullanılacağı anlamına gelir.

için MaxVectorTBitWidth sistem varsayılanı, donanımın tam olarak desteklediğinden daha dar olabilir, bu nedenle Vector<T> otomatik olarak kullanılabilir en geniş vektöre büyümez. Örneğin, Vector512<T>.IsHardwareAcceleratedtrue olabilirken Vector<T> 256 bit olarak kalır; Vector<T> öğesinin daha geniş bit genişliğini kullanmasını sağlamak için DOTNET_MaxVectorTBitWidth=512 ayarlayın.

PreferredVectorBitWidth, IsHardwareAccelerated raporlayan en büyük vektör genişliğini sınırlandırır. Bunu donanımın desteklediği düzeyin altına düşürmek, daha geniş bit genişliklerini devre dışı bırakır: 512 bitlik vektörleri destekleyen bir makinede DOTNET_PreferredVectorBitWidth=256, Vector512<T>.IsHardwareAccelerated öğesinin false bildirmesine neden olur. Genel bir düğmedir, ancak bugün yalnızca x86/x64 128'in üzerinde genişlikler sunar, bu nedenle gözlemlenebilir etkisi olan tek yer budur.

x86/x64 yönerge kümelerinin her mantıksal grubu için de ayrı bir anahtar vardır:

Düğme (DOTNET_ ön ek) Varsayılan Gates
EnableAVX 1 AVX ve bağımlılar
EnableAVX2 1 AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE ve bağımlılar
EnableAVX512 1 AVX-512 F+BW+CD+DQ+VL ve buna bağlı olanlar
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 (genişletilmiş genel amaçlı yazmaçlar)
EnableAES 1 AES, PCLMULQDQ
EnableAVX512VP2INTERSECT 1 AVX-512 VP2INTERSECT
EnableAVXIFMA 1 AVX-IFMA
EnableAVXVNNI 1 AVX-VNNI
EnableAVXVNNIINT 1 VEX AVX-VNNI-INT8 ve AVX-VNNI-INT16
EnableGFNI 1 GFNI
EnableSHA 1 SHA
EnableVAES 1 VAES, VPCLMULQDQ
EnableWAITPKG 1 WAITPKG
EnableX86Serialize 1 X86 SERİLEŞTİRME

Arm64'te, yönerge kümelerinin her mantıksal grubunun kendine özgü bir anahtarı vardır:

Düğme (DOTNET_ ön ek) Varsayılan Gates
EnableArm64Aes 1 Gelişmiş Şifreleme Standardı (AES)
EnableArm64Atomics 1 Büyük Sistem Uzantıları (LSE) atomikleri
EnableArm64Crc32 1 CRC32
EnableArm64Dczva 1 DC ZVA önbellek sıfırlama
EnableArm64Dp 1 NoktaLı Ürün
EnableArm64Rdm 1 Yuvarlamalı Çarpma-Toplama (RDM)
EnableArm64Sha1 1 SHA1
EnableArm64Sha256 1 SHA256
EnableArm64Rcpc 1 Sürüm Tutarlı, işlemciyle tutarlı sıralama (RCpc)
EnableArm64Rcpc2 1 RCpc2
EnableArm64Cssc 0 Ortak Kısa Sıralı Sıkıştırma (CSSC)
EnableArm64Sve 1 Ölçeklenebilir Vektör Uzantısı (SVE)
EnableArm64Sve2 1 SVE2
EnableArm64Sha3 1 SHA3
EnableArm64Sm4 1 SM4
EnableArm64SveAes 1 SVE AES
EnableArm64SveSha3 1 SVE SHA3
EnableArm64SveSm4 1 SVE SM4

Varsayılan değeri 0 olan bir ayar (örneğin EnableAVX10v2 veya EnableArm64Cssc), henüz tam olarak devreye alınmakta olan bir komut kümesinin etkinleştirilmesini kontrol eder; bu nedenle, siz özellikle etkinleştirene kadar devre dışı kalır.

Kazanmayı onaylamak için karşılaştırma

Vektörleştirme karmaşıklık ekler; bu yüzden kullanmaya devam etmeden önce gerçekten karşılığını verip vermediğini ölçün. BenchmarkDotNet'i kullanın ve daha önce gösterilen ortam değişkenlerini kullanarak skaler, Vector128ve Vector256 uygulamaları tek bir çalıştırmada karşılaştırın. BenchmarkDotNet'in ayrıştırma tanılayıcısı, yüksek performanslı kodu ayarlarken çok değerli olan oluşturulan derlemeyi de gösterebilir.

Göz önünde bulundurmak gereken birkaç şey:

  • Daha büyük girişler daha fazla avantaj sağlar. Küçük arabellekler için vektörleştirilmiş kod, kurulum yükü nedeniyle skaler koddan daha yavaş olabilir. Arayanlarınızın gerçekte kullandığı giriş boyutlarını karşılaştırma.
  • Hızlanmalar nadiren kusursuzdur. 32 bit öğeler üzerinde çalışan 256 bit vektör güvenilir bir şekilde 8 kat daha hızlı olmaz; bellek aktarım hızı, hizalama ve yönerge gecikme süresi tüm faktörlerini dikkate alır.
  • Bellek hizalama kararlılığı etkiler. Rastgele atama hizalaması, çalıştırmalar arasında gürültüye neden olur. Kararlı sonuçlar için AlignedAlloc ile hizalanmış bellek ayırabilir veya tam dağılımı gözlemlemek için BenchmarkDotNet'in bellek rastgeleleştirmesini etkinleştirebilirsiniz.

En iyi uygulamalar

  • Önce mevcut üst düzey API'lere ulaşın. Span<T>, string, LINQ, TensorPrimitives ve tensor türleri, birçok yaygın işlemi sizin için zaten hızlandırıyor; zaten optimize edilmiş ve test edilmiş olanları sıfırdan kendiniz yazmayın.
  • Vector128<T> ile başlayın; en geniş donanım yelpazesinde hızlandırma desteklenir ve doğru, taşınabilir bir uygulama elde etmek için Vector256<T> gerekmez. Yalnızca ölçülen sık erişimli yollar için daha geniş genişlikler ve donanım iç bilgileri ekleyin.
  • Bunları önbelleğe almak yerine doğrudan denetleyin IsHardwareAccelerated ve Count JIT bunları sabitlere dönüştürür.
  • Döngünün kalan kısmını her zaman işleyin ve veri yazarken çakışan kaynak ve hedef arabelleklerini göz önünde bulundurun.
  • Önce kenar örneği testleri yazın, sonra bir skaler çözüm yazın, ardından bu skaler mantığı vektör API'leriyle ifade edin.
  • Eklenen karmaşıklığı işlemeden önce her kod yolunu (erişim ihlalleri dahil) test edin ve gerçekçi giriş boyutlarını kıyaslayın.

Ayrıca bakınız