Gunakan SIMD dan fungsi intrinsik perangkat keras di .NET

SIMD (instruksi tunggal, beberapa data) adalah dukungan perangkat keras untuk menerapkan satu operasi ke beberapa bagian data secara paralel dengan satu instruksi. Kode yang divektorisasi memproses beberapa nilai per iterasi, bukan hanya satu, yang dapat sangat meningkatkan throughput untuk jenis pekerjaan numerik, ilmiah, grafis, pemrosesan teks, dan paralel data yang melibatkan pengulangan operasi yang sama pada suatu buffer. Konsekuensinya adalah kompleksitas tambahan, jadi hal ini paling menguntungkan ketika input cukup besar dan keuntungannya dipastikan melalui pengukuran.

.NET menyediakan beberapa jenis dukungan SIMD. Pilih salah satu yang cocok dengan berapa banyak kontrol yang Anda butuhkan dan berapa banyak kompleksitas yang ingin Anda ambil.

Jenis dukungan SIMD di .NET

API Namespace Kapan harus menggunakannya
Jenis vektor dan matriks tujuan tetap System.Numerics Grafika dan matematika geometri dengan vektor 2–4 elemen, matriks, kuaternion, dan bidang.
Vector<T> System.Numerics Vektorisasi portabel dan lebar variabel saat Anda tidak memerlukan kontrol per platform.
Vector64<T>,Vector128<T>,Vector256<T>,Vector512<T> System.Runtime.Intrinsics Vektorisasi lintas platform dengan lebar tetap dan kontrol terperinci. Ini adalah titik awal yang direkomendasikan untuk algoritma vektorisasi baru.
Intrinsik perangkat keras System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm, System.Runtime.Intrinsics.Wasm Instruksi prosesor tertentu yang tidak disediakan oleh API tingkat lebih tinggi, untuk memeras sisa performa pada jalur eksekusi yang kritis.
TensorPrimitives System.Numerics.Tensors Operasi matematika siap pakai dan tervektorisasi pada span. Ini melakukan vektorisasi untuk Anda.

Di mana API ini tumpang tindih, api tersebut berhubungan melalui lapisan abstraksi. Tipe vektor generik adalah tipe pertukaran dasar yang digunakan oleh lapisan-lapisan lain, sehingga secara teknis merupakan level terendah: tipe lebar variabel Vector<T>, yang dapat menyesuaikan hingga lebar apa pun yang didukung oleh perangkat keras tempatnya berjalan, serta tipe lebar tetap Vector64<T> hingga Vector512<T>. Intrinsik perangkat keras khusus platform di System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm, dan System.Runtime.Intrinsics.Wasm beroperasi pada tipe-tipe tersebut, yang masing-masing dipetakan secara langsung ke satu instruksi prosesor tertentu. Operasi lintas platform yang diekspos pada jenis generik berada selangkah di atas intrinsik khusus platform, menurunkannya untuk setiap target. Yang berada pada tingkat lebih tinggi lagi adalah API terkelola yang beroperasi pada seluruh buffer—metode bervektorisasi pada Span<T> dan string, serta TensorPrimitives—yang dibangun di atas lapisan-lapisan di bawahnya, sehingga Anda memperoleh akselerasi SIMD tanpa perlu menulis semuanya secara manual. Tipe System.Numerics bentuk tetap adalah tipe praktis khusus domain untuk grafika dan geometri, bukan bagian dari stack pertukaran ini.

Sisa artikel ini bekerja melalui API ini dari tingkat tertinggi ke terendah, kemudian mencakup pengujian, tolok ukur, dan praktik terbaik.

Jenis vektor dan matriks System.Numerics

Namespace System.Numerics menyediakan tipe yang dipercepat SIMD dengan bentuk tetap:

Jenis-jenis ini secara alami cocok untuk grafika dan geometri, dan runtime mempercepat operasinya dengan instruksi SIMD jika didukung oleh perangkat keras. Contoh berikut menambahkan dua vektor:

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

Mereka juga menyediakan operasi matematika vektor umum yang biasanya Anda harapkan, seperti perkalian titik, jarak, dan pembatasan nilai:

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

Jenis matriks mendukung matematika matriks seperti transpose dan multiplikasi:

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> mewakili vektor lebar variabel dari jenis numerik primitif. Panjangnya tetap selama masa aktif proses, tetapi nilai Vector<T>.Count bergantung pada CPU yang mengeksekusi kode. Kompiler Just-In-Time (JIT) memperlakukan sebagai konstanta Count , sehingga perulangan yang ditulis terhadapnya dioptimalkan dengan baik.

Vector<T> memberikan Anda vektorisasi portabel tanpa kode per platform, namun konsekuensinya adalah tidak dapat mengetahui lebar vektor pada waktu kompilasi. Contoh berikut menghitung penjumlahan dua array secara elemen:

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

Nota

Contoh ini ilustrasi. Anda jarang perlu menulis perulangan seperti ini dengan tangan, karena TensorPrimitives sudah menyediakan matematika berbasis rentang yang dipercepat. Penjumlahan per elemen ini adalah Add, dan operasi reduksi seperti Sum juga tersedia. Operasi ini dipercepat perangkat keras untuk jenis elemen yang Vector<T> mendukung (Vector<T>.IsSupported).

Periksa akselerasi perangkat keras

Jenis yang dipercepat SIMD berfungsi bahkan pada konfigurasi perangkat keras atau JIT yang tidak mendukung SIMD, karena kembali ke implementasi perangkat lunak yang tidak dipercepat. Untuk menentukan apakah akselerasi memang tersedia, periksa properti IsHardwareAccelerated yang relevan:

Properti ini diubah menjadi konstanta oleh JIT, sehingga cabang yang tidak Anda ambil dihilangkan dan tidak ada biaya runtime untuk memeriksanya. Jangan cache nilai; baca langsung di mana Anda membutuhkannya. Hal yang sama berlaku untuk Count properti (misalnya, Vector128<T>.Count), yang juga merupakan konstanta waktu JIT.

Sebagian besar operasi pada lebar yang dipercepat sendiri dipercepat, tetapi tidak dijamin untuk setiap operasi. Misalnya, pembagian bilangan titik-mengambang dapat dipercepat, sedangkan pembagian bilangan bulat tidak. Ketika Vector256 dipercepat, Vector128 biasanya juga, tetapi tidak ada jaminan, jadi periksa setiap lebar yang Anda gunakan.

Tip

Jika operasi yang Anda butuhkan tidak dipercepat pada platform yang Anda pedulikan, atau Anda menginginkan API lintas platform baru, ajukan masalah pada dotnet/runtime. Hal yang sama berlaku untuk peningkatan codegen.

Tidak setiap jenis elemen valid untuk setiap vektor. Vector128<T> dan jenis sejenisnya saat ini mendukung tipe numerik primitif (byte, sbyte, short, ushort, int, uint, long, ulong, float, double, nint, dan nuint), dan kumpulan tersebut mungkin akan bertambah hingga mencakup tipe lain di masa mendatang. Gunakan Vector128<T>.IsSupported untuk menentukan apakah T tertentu valid, yang terutama berguna dalam kode generik.

Jenis yang tidak didukung, seperti char dan bool, masih dapat di vektorisasi dengan menginterpretasikan kembali buffer sebagai jenis yang didukung dengan ukuran yang sama. Gunakan Cast untuk meninterpretasikan kembali rentang—misalnya, char untuk ushort—atau metode vektor As<TFrom, TTo> untuk menginterpretasikan kembali vektor yang sudah Anda pegang. Reinterpretasi hanya mengubah jenisnya, bukan bit-bit yang mendasarinya, jadi Anda bertanggung jawab untuk memastikan data tetap valid: bool harus tetap berupa 0 atau 1, dan char harus tetap menjadi unit kode UTF-16 yang valid. Jika operasi vektorisasi dapat menghasilkan nilai di luar rentang, berhati-hatilah untuk menormalkan hasilnya sebelum menulisnya kembali.

Vektorisasi lintas platform dengan Vector128

Vector128<T> adalah penyebut umum di setiap platform yang mendukung vektorisasi, jadi ini adalah tempat terbaik untuk memulai. Ini menyimpan vektor 128-bit: 16 byte, 8 short, 4 int atau float, atau 2 long atau double.

------------------------------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> dua kali lebih lebar, dan Vector512<T> dua kali lagi. Tidak semua perangkat keras mendukung lebar yang lebih besar, sehingga contoh yang mengikuti penggunaan Vector128 untuk portabilitas.

Untuk setiap lebar, terdapat tipe generik (Vector128<T>) untuk data dan kelas statis non-generik (Vector128) yang memuat sebagian besar operasi, termasuk metode pabrik statis seperti Create dan Load. Operator seperti +, &, dan << adalah cara idiomatik untuk mengekspresikan operasi aritmatika dan bit; lebih memilihnya daripada metode bernama yang setara untuk menghindari bug prioritas operator dan meningkatkan keterbacaan. Untuk algoritma yang bergantung pada urutan byte, gunakan percabangan berdasarkan IsLittleEndian, yang juga diubah oleh JIT menjadi konstanta.

Nota

Pada x86/x64, Vector256<T> operasi umumnya diperlakukan sebagai dua "jalur" independen 128-bit. Untuk sebagian besar operasi per elemen, hal ini transparan, tetapi operasi yang lintas jalur (seperti pengacakan atau operasi pairwise/horizontal) mungkin berperilaku berbeda atau memerlukan biaya lebih besar daripada Vector128 yang setara. Konfirmasikan dengan tolok ukur sebelum berasumsi vektor yang lebih luas lebih cepat.

Operasi penyeberangan jalur tidak melebar secara gratis

Operasi per elemen tidak bergantung pada lebar: v1 + v2 adalah hasil per elemen yang sama, baik v1 dan v2 adalah Vector128<T> maupun Vector256<T>—pelebaran hanya memproses satu lajur data tambahan per instruksi. Add pada v = [a, b, c, d] dan w = [e, f, g, h] selalu menggabungkan elemen dengan indeks yang sama:

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

Operasi lintas-lajur tidak dapat diskalakan dengan sesederhana itu, karena elemen mana yang digabungkan bergantung pada lebar vektor. Reduksi berpasangan menggabungkan elemen berdampingan, bukan elemen dengan indeks yang sama, sehingga pelebaran mengubah elemen mana yang akhirnya dipasangkan:

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)

Itulah tepatnya yang dilakukan reduksi horizontal: menjumlahkan elemen-elemen vektor melalui dua putaran penjumlahan berpasangan. Pada x86/x64, Vector128<float> (4 elemen) memberi Anda dua panggilan ke 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();
}

Terapkan pola dua pemanggilan yang sama pada Vector256<float> (8 elemen), dan hasilnya tampak benar—padahal tidak. HorizontalAdd tidak beroperasi di seluruh vektor 256-bit; ini mengulangi pola berpasangan secara independen dalam setiap jalur 128-bit. Dua putaran menghasilkan jumlah pada lane bawah (elemen 0-3) yang direplikasi di seluruh lane bawah dan jumlah pada lane atas (elemen 4-7) yang direplikasi di seluruh lane atas, bukan jumlah kedelapan elemen:

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

Untuk mendapatkan total yang benar, jembatani batas jalur secara eksplisit: baca jumlah parsial setiap jalur dengan GetLower/GetUpper dan tambahkan bersama-sama —GetLower dan GetUpper bagi vektor menjadi bagian pertama dan kedua:

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

Langkah tambahan itulah ongkos sebenarnya dari berpindah jalur. Algoritma lintas-lajur tidak serta-merta dapat melebar tanpa biaya tambahan seperti algoritma per elemen—lakukan pengukuran sebelum berasumsi bahwa vektor yang lebih lebar akan unggul.

Operasi umum

Vector128 dan varian lain yang lebih luas menyediakan cakupan API yang luas. Anda tidak perlu menghafalnya—kenali kategori dan cari detailnya saat Anda membutuhkannya. Setiap operasi memiliki alternatif perangkat lunak untuk platform yang tidak dapat melakukan akselerasi untuk operasi tersebut. Tabel berikut ini pada dasarnya mencakup seluruh permukaan.

Kategori Apa fungsinya API Representatif
Constants Vektor konstanta yang telah ditentukan sebelumnya Zero, One, NegativeOne, AllBitsSet, Indices, SignSequence, E, Pi, Tau, Epsilon, NaN, PositiveInfinity, NegativeInfinity, NegativeZero
Pembuatan Menyiarkan skalar, mengatur elemen, atau menghasilkan urutan Create, CreateScalar, CreateScalarUnsafe, Create(ReadOnlySpan<T>), CreateSequence, CreateGeometricSequence, CreateHarmonicSequence, CreateAlternatingSequence
Muat dan simpan Memindahkan data antara memori dan vektor Load, LoadUnsafe, LoadAligned, LoadAlignedNonTemporal, Store, StoreUnsafe, StoreAligned, StoreAlignedNonTemporal, CopyTo, TryCopyTo
Arithmetic Matematika per elemen dan operasi reduksi Add (x + y), Subtract (x - y), Multiply (x * y), Divide (x / y), Negate (-x), AddSaturate, SubtractSaturate, Abs, Sqrt, FusedMultiplyAdd, Dot, Sum
Operasi bit Logika dan pergeseran bitwise BitwiseAnd (x & y), BitwiseOr (x \| y), Xor (x ^ y), AndNot (x & ~y), OnesComplement (~x), ShiftLeft (x << n), ShiftRightArithmetic (x >> n), ShiftRightLogical (x >>> n)
Min, maks, dan penjepit Penjepitan minimum, maksimum, dan rentang per elemen Min, Max, Clamp, MinMagnitude, MaxMagnitude, MinNumber, MaxNumber, MinMagnitudeNumber, MaxMagnitudeNumber
Pembulatan Membulatkan setiap elemen ke nilai integral Ceiling,Floor,Round,Truncate
Fungsi matematika Tanda, interpolasi, sudut, dan fungsi bantu transendental CopySign, Lerp, DegreesToRadians, RadiansToDegrees, Hypot, Sin, Cos, SinCos, Asin, Exp, Log, Log2
Perbandingan Bandingkan setiap elemen; hasilnya adalah masker vektor, bukan bool yang diberikan operator Equals, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual
Classification Predikat per elemen di atas baris angka, masing-masing mengembalikan masker vektor IsNaN, IsFinite, IsInfinity, IsPositiveInfinity, IsNegativeInfinity, IsInteger, IsEvenInteger, IsOddInteger, IsNegative, IsPositive, IsNormal, IsSubnormal, IsZero
Pengurangan jumlah perbandingan Ringkas perbandingan per elemen menjadi satu bool EqualsAll (x == y), EqualsAny, GreaterThanAll, GreaterThanAny, GreaterThanOrEqualAll, GreaterThanOrEqualAny, LessThanAll, LessThanAny, LessThanOrEqualAll, LessThanOrEqualAny
Predikat seluruh vektor Reduksi vektor menjadi bool: apakah semua, salah satu, atau tidak ada elemen yang sama dengan suatu nilai, atau (bentuk WhereAllBitsSet) semua bitnya disetel. Utamakan ini daripada mengubah mask menjadi indeks All, Any, None, AllWhereAllBitsSet, AnyWhereAllBitsSet, NoneWhereAllBitsSet
Search Menghitung atau menemukan elemen menurut nilai, atau ( WhereAllBitsSet formulir) mengatur jalur dalam masker Count, IndexOf, LastIndexOf, CountWhereAllBitsSet, IndexOfWhereAllBitsSet, LastIndexOfWhereAllBitsSet
Masker untuk diindeks Ubah mask perbandingan menjadi bitmask skalar lalu pindai bitmask tersebut ExtractMostSignificantBits dengan TrailingZeroCount atau LeadingZeroCount
Seleksi Gabungkan dua vektor berdasarkan masker, bit demi bit ConditionalSelect(x, y, z), setara dengan (y & x) \| (z & ~x)
Conversion Ubah jenis numerik, komputasi nilai baru (misalnya, int ke float) ConvertToInt32, ConvertToInt64, ConvertToUInt32, ConvertToUInt64, ConvertToSingle, ConvertToDouble
Pelebaran dan penyempitan Membagi elemen menjadi jenis yang lebih luas atau mengemasnya ke dalam yang lebih sempit Widen, WidenLower, WidenUpper, Narrow, NarrowWithSaturation
Reinterpretasi Menginterpretasikan ulang bit sebagai jenis elemen lain tanpa mengubahnya As<TFrom, TTo>, , AsByteAsInt32, AsSingle, dan bentuk elemen lainnya As*
Interop System.Numerics Menginterpretasikan ulang antara Vector128<T> dan jenis numerik bentuk tetap AsVector, AsVector2, AsVector3, AsVector4, AsPlane, AsQuaternion, AsVector128, AsVector128Unsafe
Urus ulang Susun ulang elemen menurut indeks atau selang-selingkan dua vektor Shuffle, Reverse, Zip, ZipLower, ZipUpper, Unzip, UnzipEven, UnzipOdd, ConcatLowerLower, ConcatLowerUpper, ConcatUpperLower, ConcatUpperUpper
Akses lajur Membaca atau mengganti elemen dan bagian individual, atau mengubah ukuran vektor GetElement, WithElement, ToScalar, GetLower, GetUpper, WithLower, WithUpper, ToVector256

Tip

Beberapa operasi memiliki varian Estimate dan Native—misalnya, MultiplyAddEstimate, ClampNative, MinNative, MaxNative, ShuffleNative, dan ConvertToInt32Native. Instruksi ini dipetakan ke instruksi perangkat keras yang lebih cepat, tetapi mengorbankan sebagian presisi atau menghilangkan jaminan IEEE untuk kasus tepi tertentu (seperti penanganan NaN), jadi gunakan hanya jika pengujian tolok ukur menunjukkan bahwa bentuk yang persis itu merupakan kendala utama dan semantik yang lebih longgar dapat diterima.

Nota

Vector256.Shuffle memperlakukan inputnya sebagai satu vektor 256-bit, sedangkan Avx2.Shuffle yang spesifik platform beroperasi sebagai dua lajur 128-bit yang independen. API lintas platform adalah pilihan yang lebih portabel, tetapi konfirmasikan perilaku yang Anda butuhkan saat mentransfer intrinsik tulisan tangan.

Menyusun jalur kode

Metode yang divektorisasi biasanya bercabang menjadi jalur untuk setiap lebar vektor, serta jalur cadangan skalar untuk input kecil dan perangkat keras tanpa akselerasi. Untuk menggunakan vektor terbesar yang didukung perangkat keras, periksa vektor paling lebar terlebih dahulu lalu lanjutkan ke yang lebih kecil:

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

Guard luar untuk setiap lebar menggabungkan Vector128.IsHardwareAccelerated (konstanta pada waktu JIT yang menunjukkan apakah platform mempercepat lebar tersebut) dengan Vector128<T>.IsSupported (apakah tipe elemen T valid untuk lebar tersebut). Dalam blok yang didukung, bandingkan panjang input dengan Count untuk memilih antara jalur tervektorisasi dan mekanisme cadangan untuk input kecil. Metode ini bersifat generik terhadap T, dan blok Vector256 serta Vector512—identik dengan blok Vector128, tetapi menggunakan tipe yang lebih luas—ditampilkan dalam bentuk komentar demi keringkasan.

Ada dua mekanisme cadangan yang berbeda. Buffer yang terlalu kecil bahkan untuk vektor tersempit sekalipun, tetapi pada perangkat keras yang dipercepat, beralih ke SumVectorSmall—tabel lompatan switch yang eksplisit yang menangani setiap kemungkinan panjang sub-vektor tanpa perulangan:

// 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) juga merupakan konstanta pada waktu JIT, jadi SumVectorSmall melakukan dispatch berdasarkan lebar elemen ke tabel yang ukurannya disesuaikan untuk jumlah elemen sebanyak yang dapat dimuat oleh vektor terlebar—pendekatan yang sama seperti yang digunakan TensorPrimitives. (Ketika model keamanan memori baru diaktifkan, sizeof(T) ekspresi pada parameter jenis dengan unmanaged batasan diizinkan dalam kode aman.) Hanya tabel 4 byte yang ditampilkan; tabel 1-byte, 2-byte, dan 8-byte berbagi bentuknya. Pada kasus yang lebih besar, sisa tersebut dilipat dengan Vector256 atau Vector128 menggunakan dua pemuatan yang saling tumpang tindih—satu dari awal, satu dari akhir—sehingga penanganan sisa yang lebih lebar untuk jalur Vector512/Vector256 yang dihilangkan berada langsung di tabel lompatan. Kedua pemuatan saling tumpang tindih setiap kali panjangnya bukan kelipatan tepat dari lebarnya, sehingga bagian ekor dimask ke identitas aditif dengan ConditionalSelect sebelum dijumlahkan. Mask itu hanya diperlukan karena penjumlahan tidak bersifat idempoten; operasi yang idempoten seperti pencarian dapat langsung menggabungkan bagian akhir yang tumpang tindih. Buffer pada perangkat keras yang sama sekali tidak memiliki vektorisasi akan beralih ke SumScalar, loop skalar biasa.

Lakukan iterasi pada masukan dan tangani sisa data

Untuk memproses buffer yang lebih besar daripada satu vektor, lakukan iterasi pada buffer tersebut satu vektor tiap kali, lalu proses elemen yang tersisa yang tidak cukup untuk membentuk satu vektor penuh. Cara yang andal untuk menangani ekor tersebut adalah dengan memproses ulang elemen sebanyak satu vektor penuh terakhir, sehingga sebagian elemennya bertumpang-tindih dengan yang sudah ditangani oleh loop—dan ini menghindari epilog skalar terpisah. Apakah tumpang tindih perlu dikoreksi tergantung pada operasi.

Operasi non-idempoten seperti penjumlahan akan menghitung elemen-elemen yang tumpang tindih dua kali, jadi samarkan elemen-elemen tersebut menjadi identitas operasi sebelum menggabungkannya. Gunakan ini ketika setiap elemen harus berkontribusi persis sekali:

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

Versi ini memberi pengaman pada loop yang di-unroll dengan if, sehingga payload berukuran kecil sepenuhnya melewati empat akumulator dan langsung masuk ke penanganan sisa. Ketika data cukup, sebuah do/while mengakumulasikan empat vektor per iterasi ke dalam akumulator yang independen—yang memungkinkan prosesor menjalankan penambahan secara berjalur—dan menggabungkannya secara berpasangan. Tabel lompatan switch kemudian menggabungkan sisa nol hingga tiga vektor penuh dan, di case 0, ekor sub-vektor: tabel ini menggunakan kembali vektor penuh yang telah dimuat sebelumnya dari akhir buffer, yang menimpa elemen yang sudah diproses, dan memask bagian yang bertumpang tindih menjadi identitas aditif dengan ConditionalSelect sehingga ekor tetap tervektorisasi alih-alih beralih ke loop skalar. Seperti sebelumnya, Vector128.Create membaca elemen Vector128<T>.Count dari span. JIT menghilangkan pemeriksaan batas span untuk pola akses yang umum, jadi Create merupakan pilihan default yang baik bahkan dalam loop yang sering dieksekusi; LoadUnsafe (dibahas selanjutnya) adalah alternatif tingkat lebih rendah saat Anda menelusuri buffer menggunakan referensi terkelola. Untuk input yang sangat besar, implementasi lengkap juga akan menyelaraskan buffer dan menggunakan beban dan penyimpanan non-temporal untuk menghindari penggusuran baris cache yang berguna—baik yang dihilangkan di sini maupun dicakup sepenuhnya oleh TensorPrimitives.

Operasi idempoten seperti pencarian nilai dapat memproses ulang bagian yang tumpang tindih dengan aman, sehingga vektor terakhir langsung digabungkan tanpa mask:

// Idempotent search that re-processes the final vector instead of a scalar loop.
public static bool Contains(ReadOnlySpan<int> buffer, int searched)
{
    Debug.Assert(Vector128.IsHardwareAccelerated);

    Vector128<int> values = Vector128.Create(searched);
    ReadOnlySpan<int> remaining = buffer;

    while (remaining.Length >= Vector128<int>.Count)
    {
        if (Vector128.EqualsAny(Vector128.Create(remaining), values))
        {
            return true;
        }
        remaining = remaining.Slice(Vector128<int>.Count);
    }

    if (remaining.IsEmpty)
    {
        return false;
    }

    // A partial vector remains. When the buffer holds at least one full vector,
    // re-check the last one (overlapping the tail); otherwise scan the few elements directly.
    if (buffer.Length >= Vector128<int>.Count)
    {
        Vector128<int> tail = Vector128.Create(buffer.Slice(buffer.Length - Vector128<int>.Count));
        return Vector128.EqualsAny(tail, values);
    }

    foreach (int value in remaining)
    {
        if (value == searched)
        {
            return true;
        }
    }

    return false;
}

Warning

Salah menangani sisanya adalah sumber bug umum. Perulangan yang membaca melampaui akhir buffer menghasilkan hasil yang tidak deterministik dan dapat menyebabkan aplikasi macet. Rangkaian pengujian runtime menggunakan pembantu BoundedMemory yang menempatkan halaman yang tidak dapat diakses tepat setelah buffer, sehingga pembacaan di luar batas akan memicu AccessViolationException saat pengujian. Selalu tutupi logika yang tersisa, termasuk buffer yang panjangnya bukan kelipatan lebar vektor.

Muat dan simpan vektor dengan aman

Untuk sebagian besar kode, Vector128.Create(span) dan CopyTo merupakan cara paling sederhana untuk memindahkan data antara rentang dan vektor, dan JIT membuatnya efisien. Saat Anda memerlukan operasi baca dan tulis tingkat rendah—misalnya, untuk menelusuri buffer dengan referensi terkelola—gunakan overload LoadUnsafe dan StoreUnsafe yang menerima referensi terkelola dan offset elemen nuint. Tidak seperti overload Load/Store berbasis pointer, versi ini tidak mengharuskan Anda menyematkan buffer, dan tidak seperti aritmetika referensi mentah, versi ini tidak mengharuskan Anda menggeser ref secara manual. Kedua alternatif ini mudah salah dengan cara yang memperkenalkan lubang pengumpul sampah atau pelanggaran akses.

Sehingga buffer kosong tidak melemparkan, mendapatkan referensi awal dari GetReference (atau GetArrayDataReference untuk array) daripada ref span[0].

Penting

Aritmetika offset menggunakan nuint tak bertanda. Selalu periksa panjang buffer sebelum menghitung offset seperti buffer.Length - Vector128<int>.Count. Jika buffer lebih kecil daripada satu vektor, pengurangan tersebut mengalami underflow sehingga menghasilkan nilai yang sangat besar, dan perulangan membaca memori yang tidak valid.

Intrinsik perangkat keras khusus platform

Ketika instruksi prosesor tertentu memberi Anda keunggulan yang tidak disediakan oleh API portabel, gunakan intrinsik perangkat keras di System.Runtime.Intrinsics.X86, System.Runtime.Intrinsics.Arm, dan System.Runtime.Intrinsics.Wasm. Setiap kelas intrinsik memiliki IsSupported properti (juga konstanta JIT) sehingga Anda dapat menjaga jalur khusus dan kembali ke kode portabel di tempat lain:

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

Metode sebelumnya menunjukkan cara menerangi jalur kode per arsitektur saat Anda menginginkannya, tetapi sengaja merupakan contoh sederhana: Anda tidak benar-benar membutuhkannya di sini. Ekspresi (vector & mask) == Vector128<byte>.Zero portabel sudah menurunkan ke instruksi optimal pada setiap platform (misalnya, ptest pada x86/x64), sehingga melakukan pekerjaan yang sama dengan cabang yang ditulis tangan, hanya tanpa kompleksitas. Gunakan intrinsic eksplisit hanya jika instruksi tertentu terbukti memberikan hasil yang lebih baik daripada yang dihasilkan oleh API portabel.

Intrinsik perangkat keras memerlukan implementasi terpisah untuk setiap set instruksi, jadi anggap ini sebagai pengoptimalan untuk jalur eksekusi yang telah diukur dan terbukti kritis, bukan sebagai pilihan bawaan. Vector128 / Vector256 API-API tersebut sudah dikompilasi menjadi instruksi yang efisien di setiap platform, dan dalam praktiknya kode canggih untuk setiap instruksi tidak selalu lebih unggul. Konfirmasikan perbedaan dengan tolok ukur sebelum berkomitmen pada pemeliharaan tambahan.

Matematika tingkat lebih tinggi dengan TensorPrimitives

Jika Anda memerlukan operasi matematika tervektorisasi pada span dan tidak ingin menulis perulangannya sendiri, TensorPrimitives menyediakan serangkaian besar operasi numerik—aritmetika per elemen, operasi eksponensial, dan reduksi seperti hasil kali titik dan kemiripan kosinus—yang secara internal sudah divektorisasi. Ini tersedia dalam paket 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);

Untuk beban kerja AI dan numerik, TensorPrimitives kerap memberikan sebagian besar manfaat SIMD yang ditulis secara manual tanpa kerumitannya.

Menguji semua jalur kode

Karena metode yang divektorisasi memiliki beberapa jalur kode, pengujian perlu mencakup semuanya: jalur Vector256, jalur Vector128, dan jalur skalar, masing-masing dengan input yang cukup besar maupun terlalu kecil untuk memperoleh manfaat. Anda dapat memvariasi ukuran input dalam pengujian, tetapi Anda tidak dapat mengalihkan akselerasi perangkat keras di tingkat pengujian. Sebagai gantinya, kontrol dengan variabel lingkungan sebelum proses dimulai:

  • Atur DOTNET_EnableAVX2=0 agar Vector256.IsHardwareAccelerated mengembalikan false.
  • Atur DOTNET_EnableHWIntrinsic=0 untuk menonaktifkan intrinsik sepenuhnya, sehingga Vector128, Vector64, dan Vector<T> semuanya melaporkan tidak ada akselerasi.

Untuk menguji setiap jalur pada satu mesin, jalankan rangkaian pengujian sekali tanpa override, sekali dengan DOTNET_EnableAVX2=0, dan sekali dengan DOTNET_EnableHWIntrinsic=0. Alternatifnya adalah mengujinya pada perangkat keras yang cukup beragam untuk mencakup semuanya.

Opsi konfigurasi set instruksi

Selain kedua hal tersebut, runtime mengenali opsi untuk setiap pengelompokan logis kumpulan instruksi, yang masing-masing diawali dengan DOTNET_. Satu kenop dapat mencakup beberapa set instruksi terkait—EnableAVX2, misalnya, gerbang AVX2 bersama dengan BMI1, BMI2, F16C, FMA, LZCNT, dan MOVBE. Mengatur kenop ke 0 akan menonaktifkan seluruh grup dan semua yang dilapiskan di atasnya. Mengaturnya ke 1 (nilai bawaan bagi kebanyakan orang) akan mengaktifkan grup tersebut, tetapi perangkat keras tetap harus benar-benar mendukungnya—mengaktifkan opsi yang tidak didukung oleh CPU saat ini akan diabaikan, jadi Anda hanya bisa membatasi apa yang digunakan, bukan memaksa instruksi yang tidak didukung untuk aktif lalu merusak sistem Anda. DOTNET_EnableHWIntrinsic=0 adalah langkah paling drastis—menonaktifkan semuanya hingga ke tingkat paling dasar, sehingga Vector128, Vector64, dan Vector<T> semuanya melaporkan tidak ada akselerasi dan kode beralih ke jalur perangkat lunak.

Penting

Ini adalah alat diagnostik, yang terutama ditujukan untuk pengujian dan validasi—menguji setiap jalur kode, mereproduksi masalah yang spesifik pada perangkat keras, atau mengonfirmasi mekanisme cadangan. Mereka tidak dirancang untuk penggunaan umum atau produksi, dan mereka bukan kontrak stabilitas. Rangkaian berikut adalah yang dikenali oleh .NET 11; rilis sebelumnya memiliki rangkaian yang berbeda—opsi baseline dan AVX-512 khususnya telah dikonfigurasi ulang—jadi pastikan nama-namanya sesuai dengan versi runtime yang Anda targetkan.

Alat-alat ini juga memiliki batasan pada apa yang mereka jangkau. Karena keputusan JIT bergantung pada mereka, mereka tidak memengaruhi kode yang sudah dikompilasi sebelumnya melalui ReadyToRun atau Native AOT, dan mereka juga tidak selalu memengaruhi rutinitas internal yang digunakan oleh runtime dan pustaka inti itu sendiri. Perlakukan itu sebagai cara untuk mengendalikan kode Anda sendiri yang dikompilasi JIT, bukan sebagai sakelar untuk menonaktifkan secara global suatu set instruksi.

Sakelar dasar dan batas lebar berlaku pada setiap arsitektur:

Knob (DOTNET_ awalan) Default Efek
EnableHWIntrinsic 1 Sakelar master untuk semua intrinsik perangkat keras; 0 memaksa jalur perangkat lunak sepenuhnya.
MaxVectorTBitWidth bawaan sistem Membatasi Vector<T> hingga lebar maksimum dalam bit; nilai di bawah 128 berarti default sistem.
PreferredVectorBitWidth bawaan sistem Membatasi vektor lebar tetap maksimum yang melaporkan IsHardwareAccelerated, dalam bit; nilai di bawah 128 berarti default sistem.

Setelan bawaan sistem untuk MaxVectorTBitWidth dapat lebih sempit daripada yang didukung penuh oleh perangkat keras, sehingga Vector<T> tidak secara otomatis melebar hingga vektor paling lebar yang tersedia. Misalnya, Vector512<T>.IsHardwareAccelerated dapat berupa true sementara Vector<T> tetap 256-bit; atur DOTNET_MaxVectorTBitWidth=512 untuk menetapkan Vector<T> agar menggunakan lebar yang lebih besar.

PreferredVectorBitWidth menutup lebar vektor maksimum yang melaporkan IsHardwareAccelerated. Menurunkannya hingga di bawah batas yang didukung perangkat keras akan menonaktifkan lebar yang lebih besar: pada mesin yang mendukung vektor 512-bit, DOTNET_PreferredVectorBitWidth=256 membuat Vector512<T>.IsHardwareAccelerated melaporkan false. Ini adalah parameter umum, tetapi saat ini hanya x86/x64 yang menawarkan lebar di atas 128, jadi hanya di situlah efeknya dapat diamati.

Setiap kelompok logis kumpulan instruksi x86/x64 juga memiliki opsi tersendiri:

Knob (dengan awalan DOTNET_) Default Gates
EnableAVX 1 AVX dan dependensinya
EnableAVX2 1 AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE, dan turunannya
EnableAVX512 1 AVX-512 F+BW+CD+DQ+VL dan dependensinya
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 (register tujuan umum yang diperluas)
EnableAES 1 AES, PCLMULQDQ
EnableAVX512VP2INTERSECT 1 AVX-512 VP2INTERSECT
EnableAVXIFMA 1 AVX-IFMA
EnableAVXVNNI 1 AVX-VNNI
EnableAVXVNNIINT 1 VEX AVX-VNNI-INT8 dan AVX-VNNI-INT16
EnableGFNI 1 GFNI
EnableSHA 1 SHA
EnableVAES 1 VAES, VPCLMULQDQ
EnableWAITPKG 1 WAITPKG
EnableX86Serialize 1 X86 SERIALIZE

Pada Arm64, setiap pengelompokan logis set instruksi memiliki opsi masing-masing:

Knob (prefiks DOTNET_) Default Gates
EnableArm64Aes 1 SEL
EnableArm64Atomics 1 Atom Ekstensi Sistem Besar (LSE)
EnableArm64Crc32 1 CRC32
EnableArm64Dczva 1 DC ZVA nol cache
EnableArm64Dp 1 Produk Titik
EnableArm64Rdm 1 Rounding Doubling Multiply Accumulate (RDM)
EnableArm64Sha1 1 SHA1
EnableArm64Sha256 1 SHA256
EnableArm64Rcpc 1 Pengurutan konsisten-rilis, konsisten-prosesor (RCpc)
EnableArm64Rcpc2 1 RCpc2
EnableArm64Cssc 0 Kompresi Urutan Pendek Umum (CSSC)
EnableArm64Sve 1 Ekstensi Vektor Yang Dapat Diskalakan (SVE)
EnableArm64Sve2 1 SVE2
EnableArm64Sha3 1 SHA3
EnableArm64Sm4 1 SM4
EnableArm64SveAes 1 SVE AES
EnableArm64SveSha3 1 SVE SHA3
EnableArm64SveSm4 1 SVE SM4

Opsi yang secara default disetel ke 0 (misalnya, EnableAVX10v2 atau EnableArm64Cssc) mengendalikan akses ke set instruksi yang masih dalam proses diaktifkan, sehingga tetap nonaktif sampai Anda memilih untuk mengaktifkannya.

Tolok ukur untuk memastikan kemenangan

Vektorisasi menambah kompleksitas, jadi ukur terlebih dahulu apakah manfaatnya sepadan sebelum Anda terus menggunakannya. Gunakan BenchmarkDotNet, dan gunakan variabel lingkungan yang sama yang ditunjukkan sebelumnya untuk membandingkan skalar, Vector128, dan Vector256 implementasi dalam satu eksekusi. Diagnoser disasembli BenchmarkDotNet juga dapat menghasilkan kode assembly yang dibuat, yang sangat berharga saat mengoptimalkan kode berperforma tinggi.

Beberapa hal yang perlu diingat:

  • Input yang lebih besar mendapat manfaat lebih banyak. Untuk buffer kecil, kode vektorisasi bisa lebih lambat daripada kode skalar karena overhead penyiapan. Ukur ukuran input yang benar-benar digunakan oleh pemanggil Anda.
  • Peningkatan kecepatan jarang sempurna. Vektor 256-bit yang beroperasi pada elemen 32-bit tidak serta-merta akan 8x lebih cepat secara konsisten; throughput memori, penyelarasan, dan latensi instruksi semuanya turut memengaruhi.
  • Perataan memori memengaruhi stabilitas. Perataan alokasi acak menambahkan kebisingan di antara eksekusi. Anda dapat mengalokasikan memori yang selaras dengan AlignedAlloc untuk hasil yang stabil, atau mengaktifkan pengacakan memori BenchmarkDotNet untuk mengamati distribusi penuh.

Praktik terbaik

  • Jangkau API tingkat lebih tinggi yang ada terlebih dahulu. Span<T>, string, LINQ, TensorPrimitives, dan tipe tensor sudah membantu mempercepat banyak operasi umum bagi Anda—jangan mengimplementasikan sendiri apa yang sudah dioptimalkan dan diuji.
  • Mulailah dengan Vector128<T>; ini didukung akselerasi pada berbagai perangkat keras yang paling luas, dan Anda tidak memerlukan Vector256<T> untuk mendapatkan implementasi yang benar dan portabel. Tambahkan lebar yang lebih lebar dan intrinsik perangkat keras hanya untuk jalur panas terukur.
  • Periksa IsHardwareAccelerated dan Count langsung alih-alih menyimpan cache; JIT mengubahnya menjadi konstanta.
  • Selalu tangani sisa iterasi perulangan, dan pertimbangkan buffer sumber dan tujuan yang saling tumpang tindih saat melakukan penyimpanan.
  • Tulis pengujian edge-case terlebih dahulu, lalu solusi skalar, lalu ekspreskan logika skalar tersebut dengan API vektor.
  • Uji setiap jalur kode (termasuk pelanggaran akses) dan lakukan benchmark pada ukuran input yang realistis sebelum Anda memutuskan untuk menambahkan kompleksitas tersebut.

Baca juga