Gambaran kecil tentang SIMD di .NET / C #

Berikut ini sekilas kemampuan vektorisasi algoritma dalam .NET Framework dan .NET Core. Artikel ini ditujukan bagi mereka yang tidak tahu apa-apa tentang teknik ini. Saya juga akan menunjukkan bahwa .NET tidak benar-benar ketinggalan bahasa "dikompilasi nyata" untuk pengembangan asli.


Saya baru mulai belajar teknik vektorisasi. Jadi, saya akan menghargai jika anggota masyarakat menemukan kesalahan yang jelas atau menyarankan perbaikan pada algoritma yang dijelaskan.


Beberapa sejarah


SIMD muncul di .NET Framework 4.6 pada 2015. Saat itulah tipe Matrix3x2, Matrix4x4, Plane, Quaternion, Vector2, Vector3, dan Vector4 ditambahkan. Mereka memungkinkan perhitungan vektor. Berikutnya adalah tipe Vector <T> yang memberi lebih banyak peluang untuk membuat vektor algoritma. Namun, banyak programmer masih tidak puas karena jenis ini membatasi aliran ide coders dan tidak membiarkan penggunaan kapasitas penuh instruksi SIMD dalam prosesor modern. Sekarang di .NET Core 3.0 Preview, kami memiliki System.Runtime.Intrinsics namespace yang memberikan banyak kebebasan dalam pilihan instruksi. Untuk mendapatkan kecepatan maksimal, Anda perlu menggunakan RyuJit dan menggunakan rakitan x64 atau mematikan Prefer 32-bit dan memilih rakitan AnyCPU. Saya menjalankan semua tolok ukur pada komputer CPU Intel Core i7-6700 3,40 GHz (Skylake).


Menjumlahkan elemen array


Saya memutuskan untuk memulai dengan tugas klasik yang biasanya didahulukan jika ada vektorisasi yang terlibat. Ini berkaitan dengan menemukan jumlah elemen array. Mari kita menulis empat implementasi dari tugas ini untuk menjumlahkan elemen-elemen dari Array.


Implementasi yang paling jelas:


public int Naive() { int result = 0; foreach (int i in Array) { result += i; } return result; } 

Implementasi berbasis LINQ:


 public long LINQ() => Array.Aggregate<int, long>(0, (current, i) => current + i); 

Implementasi berdasarkan vektor dari System.Numerics:


 public int Vectors() { int vectorSize = Vector<int>.Count; var accVector = Vector<int>.Zero; int i; var array = Array; for (i = 0; i <= array.Length - vectorSize; i += vectorSize) { var v = new Vector<int>(array, i); accVector = Vector.Add(accVector, v); } int result = Vector.Dot(accVector, Vector<int>.One); for (; i < array.Length; i++) { result += array[i]; } return result; } 

Implementasi berdasarkan kode dari namespace System.Runtime.Intrinsics:


 public unsafe int Intrinsics() { int vectorSize = 256 / 8 / 4; var accVector = Vector256<int>.Zero; int i; var array = Array; fixed (int* ptr = array) { for (i = 0; i <= array.Length - vectorSize; i += vectorSize) { var v = Avx2.LoadVector256(ptr + i); accVector = Avx2.Add(accVector, v); } } int result = 0; var temp = stackalloc int[vectorSize]; Avx2.Store(temp, accVector); for (int j = 0; j < vectorSize; j++) { result += temp[j]; } for (; i < array.Length; i++) { result += array[i]; } return result; } 

Saya membandingkan 4 metode tersebut di komputer saya dan mendapatkan hasil sebagai berikut:


MetodeItemCountBerartiKesalahanStddevRasio
Naif103,531 ns0,0336 ns0,0314 ns1,00
LINQ1076.925 ns0,4166 ns0,3897 ns21.79
Vektor102,750 ns0,0210 ns0,0196 ns0,78
Intrinsik106.513 ns0,0623 ns0,0582 ns1,84
Naif10047.982 ns0,3975 ns0,3524 ns1,00
LINQ100590.414 ns3,8808 ns3.4402 ns12.31
Vektor10010.122 ns0,0747 ns0,0699 ns0,21
Intrinsik10014.277 ns0,0566 ns0,0529 ns0,30
Naif1000569.910 ns2,8297 ns2.6469 ns1,00
LINQ10005,658.570 ns31,7465 ns29.6957 ns9,93
Vektor100079.598 ns0,3498 ns0,3272 ns0,14
Intrinsik100066.970 ns0,3937 ns0,3682 ns0,12
Naif10.0005,637.571 ns37.5050 ns29.2814 ns1,00
LINQ10.00056.498.987 ns294.8776 ns275.8287 ns10.02
Vektor10.000772.900 ns2,6802 ns2,5070 ns0,14
Intrinsik10.000579.152 ns2,8371 ns2,6538 ns0,10
Naif100.00056.352.865 ns230.7916 ns215.8826 ns1,00
LINQ100.000562.610.571 ns3,775.7631 ns3,152.9332 ns9,98
Vektor100.0008,389.647 ns165,9590 ns227.1666 ns0,15
Intrinsik100.0007,261.334 ns89.6468 ns69,9903 ns0,13

Sudah jelas bahwa solusi dengan Vektor dan Intrinsik jauh lebih cepat daripada solusi yang jelas dan berbasis LINQ. Sekarang kita perlu mencari tahu apa yang terjadi dalam kedua metode ini.


Mari kita pertimbangkan metode Vektor lebih dekat:


Vektor
 public int Vectors() { int vectorSize = Vector<int>.Count; var accVector = Vector<int>.Zero; int i; var array = Array; for (i = 0; i <= array.Length - vectorSize; i += vectorSize) { var v = new Vector<int>(array, i); accVector = Vector.Add(accVector, v); } int result = Vector.Dot(accVector, Vector<int>.One); for (; i < array.Length; i++) { result += array[i]; } return result; } 

  • int vectorSize = Vektor <int> .Count; - jumlah angka 4-byte yang dapat kita tempatkan dalam vektor. Jika akselerasi perangkat keras digunakan, nilai ini menunjukkan berapa banyak angka 4-byte yang dapat kita masukkan dalam satu register SIMD. Bahkan, ini menunjukkan berapa banyak elemen jenis ini dapat ditangani secara bersamaan;
  • accVector adalah vektor yang mengakumulasikan hasil fungsi;
  • var v = Vector baru <int> (array, i); - data dari array dimuat ke vektor v baru, mulai dari indeks i. Ukuran vektor data akan dimuat dengan tepat;
  • accVector = Vector.Add (accVector, v); - dua vektor dijumlahkan.
    Misalnya, ada 8 angka dalam Array: {0, 1, 2, 3, 4, 5, 6, 7} dan vectorSize == 4.
    Kemudian selama iterasi siklus pertama, accVector = {0, 0, 0, 0}, v = {0, 1, 2, 3} dan setelah penambahan accVector akan menampung: {0, 0, 0, 0} 0 {0, 1 , 2, 3} = {0, 1, 2, 3}.
    Selama iterasi kedua v = {4, 5, 6, 7} dan setelah penambahan accVector = {0, 1, 2, 3} + {4, 5, 6, 7} = {4, 6, 8, 10}.
  • Sekarang kita hanya perlu mendapatkan jumlah semua elemen vektor. Untuk melakukan ini kita dapat menggunakan perkalian skalar dengan vektor yang diisi dengan yang: int result = Vector.Dot (accVector, Vector <int> .One);
    Maka kita mendapatkan: {4, 6, 8, 10} * {1, 1, 1, 1} = 4 * 1 + 6 * 1 + 8 * 1 + 10 * 1 = 28.
  • Jika perlu, angka-angka yang tidak sesuai dengan vektor terakhir akan dijumlahkan di akhir.

Mari kita lihat kode intrinsik:


Intrinsik
 public unsafe int Intrinsics() { int vectorSize = 256 / 8 / 4; var accVector = Vector256<int>.Zero; int i; var array = Array; fixed (int* ptr = array) { for (i = 0; i <= array.Length - vectorSize; i += vectorSize) { var v = Avx2.LoadVector256(ptr + i); accVector = Avx2.Add(accVector, v); } } int result = 0; var temp = stackalloc int[vectorSize]; Avx2.Store(temp, accVector); for (int j = 0; j < vectorSize; j++) { result += temp[j]; } for (; i < array.Length; i++) { result += array[i]; } return result; } 

Kita dapat melihat bahwa itu seperti Vektor dengan satu pengecualian:


  • vectorSize ditentukan oleh konstanta. Ini karena metode ini secara eksplisit menggunakan instruksi Avx2 yang beroperasi dengan register 256-bit. Aplikasi nyata harus mencakup pemeriksaan apakah prosesor saat ini mendukung Avx2. Jika tidak, kode lain harus dipanggil. Ini terlihat seperti ini:
     if (Avx2.IsSupported) { DoThingsForAvx2(); } else if (Avx.IsSupported) { DoThingsForAvx(); } ... else if (Sse2.IsSupported) { DoThingsForSse2(); } ... 
  • var accVector = Vector256 <int> .Zero; accVector dinyatakan sebagai vektor 256-bit yang diisi dengan nol.
  • fixed (int * ptr = Array) - pointer ke array ditempatkan di ptr.
  • Berikutnya adalah operasi yang sama seperti di Vektor: memuat data ke dalam vektor dan penambahan dua vektor.
  • Penjumlahan elemen vektor menggunakan metode berikut:
    • buat array di stack: var temp = stackalloc int [vectorSize];
    • memuat vektor ke dalam array ini: Avx2.Store (temp, accVector);
    • jumlah elemen array selama siklus.
  • Selanjutnya, elemen-elemen yang tidak sesuai dengan vektor terakhir diringkas.

Membandingkan dua array


Kita perlu membandingkan dua array byte. Persis tugas inilah yang membuat saya mempelajari SIMD di .NET. Sekali lagi, mari kita menulis beberapa metode untuk pembandingan dan membandingkan dua array: ArrayA dan ArrayB.


Solusi paling jelas:


 public bool Naive() { for (int i = 0; i < ArrayA.Length; i++) { if (ArrayA[i] != ArrayB[i]) return false; } return true; } 

Solusi berbasis LINQ:


 public bool LINQ() => ArrayA.SequenceEqual(ArrayB); 

Solusi berdasarkan fungsi MemCmp:


 [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] static extern int memcmp(byte[] b1, byte[] b2, long count); public bool MemCmp() => memcmp(ArrayA, ArrayB, ArrayA.Length) == 0; 

Solusi berdasarkan vektor dari System.Numerics:


 public bool Vectors() { int vectorSize = Vector<byte>.Count; int i = 0; for (; i <= ArrayA.Length - vectorSize; i += vectorSize) { var va = new Vector<byte>(ArrayA, i); var vb = new Vector<byte>(ArrayB, i); if (!Vector.EqualsAll(va, vb)) { return false; } } for (; i < ArrayA.Length; i++) { if (ArrayA[i] != ArrayB[i]) return false; } return true; } 

Solusi berbasis intrinsik:


 public unsafe bool Intrinsics() { int vectorSize = 256 / 8; int i = 0; const int equalsMask = unchecked((int) (0b1111_1111_1111_1111_1111_1111_1111_1111)); fixed (byte* ptrA = ArrayA) fixed (byte* ptrB = ArrayB) { for (; i <= ArrayA.Length - vectorSize; i += vectorSize) { var va = Avx2.LoadVector256(ptrA + i); var vb = Avx2.LoadVector256(ptrB + i); var areEqual = Avx2.CompareEqual(va, vb); if (Avx2.MoveMask(areEqual) != equalsMask) { return false; } } for (; i < ArrayA.Length; i++) { if (ArrayA[i] != ArrayB[i]) return false; } return true; } } 

Hasil menjalankan patokan di komputer saya:


MetodeItemCountBerartiKesalahanStddevRasio
Naif10.0007.033.8 ns50,636 ns47.365 ns1,00
LINQ10.00064.841,4 ns289.157 ns270.478 ns9.22
Vektor10.000504.0 ns2,406 ns2,251 ns0,07
Memcmp10.000368.1 ns2,637 ns2,466 ns0,05
Intrinsik10.000283,6 ns1,135 ns1,061 ns0,04
Naif100.00085.214,4 ns903.868 ns845.478 ns1,00
LINQ100.000702.279,4 ns2,846.609 ns2,662.720 ns8.24
Vektor100.0005,179.2 ns45.337 ns42.409 ns0,06
Memcmp100.0004,510,5 ns24.292 ns22.723 ns0,05
Intrinsik100.0002,957,0 ns11.452 ns10.712 ns0,03
Naif1.000.000844,006,1 ns3,552.478 ns3,322.990 ns1,00
LINQ1.000.0006,483.079,3 ns42.641.040 ns39.886.455 ns7.68
Vektor1.000.00054.180.1 ns357.258 ns334.180 ns0,06
Memcmp1.000.00049,480.1 ns515.675 ns457.133 ns0,06
Intrinsik1.000.00036.633.9 ns680.525 ns636.564 ns0,04

Saya kira kode metode ini jelas, kecuali untuk dua baris dalam Intrinsik:


 var areEqual = Avx2.CompareEqual(va, vb); if (Avx2.MoveMask(areEqual) != equalsMask) { return false; } 

Pada baris pertama dua vektor dibandingkan untuk kesetaraan dan hasilnya disimpan dalam vektor areEqual di mana semua bit dalam elemen pada posisi tertentu diatur ke 1 jika elemen yang sesuai dalam va dan vb sama. Jadi, ternyata jika byte vektor va dan vb sama, semua elemen di areEqual harus sama dengan 255 (11111111b). Karena Avx2.CompareEqual adalah pembungkus di atas _mm256_cmpeq_epi8, kita dapat pergi ke situs web Intel dan melihat pseudocode dari operasi ini:
Metode MoveMask membuat angka 32-bit dari vektor. Bit teratas dari masing-masing 32 elemen onebyte dalam vektor adalah nilai bit dalam hasil MoveMask. Kodesemu tersedia di sini .


Jadi, jika beberapa byte dalam va dan vb tidak cocok, byte yang sesuai di areEqual akan menjadi 0. Oleh karena itu, bit teratas dari byte ini akan menjadi 0 juga. Ini berarti bit terkait dalam respons Avx2.MoveMask juga akan 0 dan areEqual tidak akan sama dengan equalsMask.


Mari kita lihat satu contoh dengan asumsi bahwa panjang vektor adalah 8 byte (untuk menulis lebih sedikit):


  • Misalkan va = {100, 10, 20, 30, 100, 40, 50, 100} dan vb = {100, 20, 10, 30, 100, 40, 80, 90}.
  • Maka areEquals akan menjadi {255, 0, 0, 255, 255, 255, 0, 0}.
  • Metode MoveMask akan mengembalikan 10011100b yang harus dibandingkan dengan topeng 11111111b. Karena topeng ini tidak sama, vektor va dan vb juga tidak sama.

Menghitung berapa kali suatu elemen muncul dalam koleksi.


Terkadang Anda perlu menghitung kemunculan elemen tertentu, misalnya bilangan bulat, dalam koleksi. Kami dapat mempercepat algoritme ini juga. Sebagai perbandingan, mari kita menulis beberapa metode untuk mencari elemen Item di Array.


Yang paling jelas:


 public int Naive() { int result = 0; foreach (int i in Array) { if (i == Item) { result++; } } return result; } 

Menggunakan LINQ:


 public int LINQ() => Array.Count(i => i == Item); 

Menggunakan vektor dari System.Numerics.Vectors:


 public int Vectors() { var mask = new Vector<int>(Item); int vectorSize = Vector<int>.Count; var accResult = new Vector<int>(); int i; var array = Array; for (i = 0; i <= array.Length - vectorSize; i += vectorSize) { var v = new Vector<int>(array, i); var areEqual = Vector.Equals(v, mask); accResult = Vector.Subtract(accResult, areEqual); } int result = 0; for (; i < array.Length; i++) { if (array[i] == Item) { result++; } } result += Vector.Dot(accResult, Vector<int>.One); return result; } 

Menggunakan Intrinsik:


 public unsafe int Intrinsics() { int vectorSize = 256 / 8 / 4; var temp = stackalloc int[vectorSize]; for (int j = 0; j < vectorSize; j++) { temp[j] = Item; } var mask = Avx2.LoadVector256(temp); var accVector = Vector256<int>.Zero; int i; var array = Array; fixed (int* ptr = array) { for (i = 0; i <= array.Length - vectorSize; i += vectorSize) { var v = Avx2.LoadVector256(ptr + i); var areEqual = Avx2.CompareEqual(v, mask); accVector = Avx2.Subtract(accVector, areEqual); } } int result = 0; Avx2.Store(temp, accVector); for(int j = 0; j < vectorSize; j++) { result += temp[j]; } for(; i < array.Length; i++) { if (array[i] == Item) { result++; } } return result; } 

Hasil menjalankan patokan di komputer saya:


MetodeItemCountBerartiKesalahanStddevRasio
Naif108,844 ns0,0772 ns0,0603 ns1,00
LINQ1087.456 ns0,9496 ns0,8883 ns9,89
Vektor103,140 ns0,0406 ns0,0380 ns0,36
Intrinsik1013.813 ns0,0825 ns0,0772 ns1.56
Naif100107.310 ns0,6975 ns0,6183 ns1,00
LINQ100626.285 ns5.7677 ns5.3951 ns5.83
Vektor10011.844 ns0,2113 ns0,1873 ns0,11
Intrinsik10019.616 ns0,1018 ns0,0903 ns0,18
Naif10001.032.466 ns6.3799 ns5.6556 ns1,00
LINQ10006,266.605 ns42,6585 ns39,9028 ns6.07
Vektor100083,417 ns0,5393 ns0,4780 ns0,08
Intrinsik100088,358 ns0,4921 ns0,4603 ns0,09
Naif10.0009,942.503 ns47.9732 ns40.0598 ns1,00
LINQ10.00062.305.598 ns643.8775 ns502.6972 ns6.27
Vektor10.000914.967 ns7.2959 ns6.8246 ns0,09
Intrinsik10.000931.698 ns6.3444 ns5.9346 ns0,09
Naif100.00094.834.804 ns793.8585 ns703.7349 ns1,00
LINQ100.000626.620.968 ns4,696.9221 ns4,393.5038 ns6.61
Vektor100.0009,000.827 ns179.5351 ns192.1005 ns0,09
Intrinsik100.0008,690.771 ns101.7078 ns95.1376 ns0,09
Naif1.000.000959.302.249 ns4,268.2488 ns3,783.6914 ns1,00
LINQ1.000.0006.218.681.888 ns31.321.9277 ns29.298.5506 ns6.48
Vektor1.000.00099.778.488 ns1,975.6001 ns4,252.6877 ns0,10
Intrinsik1.000.00096.449.350 ns1,171.8067 ns978.5116 ns0,10

Metode Vektor dan Intrinsik sepenuhnya bertepatan dalam logika tetapi berbeda dalam implementasi operasi tertentu. Idenya adalah sebagai berikut:


  • membuat vektor topeng di mana nomor yang diperlukan disimpan di setiap elemen;
  • muat bagian array dalam vektor v dan bandingkan bagian ini dengan mask. Akibatnya, semua bit akan disetel dalam elemen sama dengan areEqual. Karena areEqual adalah array bilangan bulat, maka jika kita mengatur semua bit dari satu elemen, kita akan mendapatkan -1 pada elemen ini ((int) (1111_1111_1111_1111_1111_1111_1111_1111b) == -1);
  • kurangi areEqual vector dari accVector. Kemudian, accVector akan menahan hitungan berapa kali elemen item terjadi di semua vektor untuk setiap posisi (dikurangi dengan minus adalah plus).

Seluruh kode dari artikel ini ada di GitHub .


Kesimpulan


Saya menjelaskan hanya sebagian kecil dari kemampuan .NET untuk vektorisasi perhitungan. Untuk melihat daftar lengkap semua intrinsik yang tersedia di .NET Core di bawah x86, buka kode sumber . Lebih mudah bahwa ringkasan setiap intrinsik dalam file C # berisi namanya di dunia C. Ini membantu memahami tujuan intrinsik ini dan transfer algoritma C ++ / C yang ada ke .NET. Dokumentasi System.Numerics.Vector tersedia di msdn .


Saya pikir .NET memiliki keunggulan dibandingkan C ++. Karena kompilasi JIT sudah terjadi pada mesin klien, kompiler dapat mengoptimalkan kode untuk prosesor klien tertentu, memberikan kinerja maksimum. Pada saat yang sama, seorang programmer dapat tetap berada dalam satu bahasa dan teknologi yang sama untuk menulis kode cepat.

Source: https://habr.com/ru/post/id467689/


All Articles