Memory and Span pt. 1

Mulai dari .NET Core 2.0 dan .NET Framework 4.5 kita dapat menggunakan tipe data baru: Span dan Memory . Untuk menggunakannya, Anda hanya perlu menginstal paket System.Memory nuget:


PM> Install-Package System.Memory

Tipe data ini penting karena tim CLR telah melakukan pekerjaan besar untuk mengimplementasikan dukungan khusus mereka di dalam kode .NET Core 2.1+ JIT compiler dengan memasukkan tipe data ini langsung ke dalam inti. Apa tipe data ini dan mengapa mereka layak satu bab?


Jika kita berbicara tentang masalah yang membuat jenis ini muncul, saya harus menyebutkan tiga dari mereka. Yang pertama adalah kode yang tidak dikelola.


Baik bahasa dan platform telah ada selama bertahun-tahun bersama dengan sarana untuk bekerja dengan kode yang tidak dikelola. Jadi, mengapa merilis API lain untuk bekerja dengan kode yang tidak dikelola jika sebelumnya pada dasarnya ada selama bertahun-tahun? Untuk menjawab pertanyaan ini, kita harus memahami kekurangan kita sebelumnya.


Bab ini diterjemahkan dari bahasa Rusia bersama oleh penulis dan penerjemah profesional . Anda dapat membantu kami dengan terjemahan dari bahasa Rusia atau Inggris ke bahasa lain, terutama ke bahasa Cina atau Jerman.

Juga, jika Anda ingin berterima kasih kepada kami, cara terbaik yang dapat Anda lakukan adalah memberi kami bintang di github atau untuk repositori garpu github / sidristij / dotnetbook .

Pengembang platform sudah mencoba memfasilitasi penggunaan sumber daya yang tidak dikelola untuk kita. Mereka menerapkan pembungkus otomatis untuk metode dan marshaling yang diimpor yang bekerja secara otomatis dalam banyak kasus. Di sini juga milik stackalloc , yang disebutkan dalam bab tentang tumpukan thread. Namun, seperti yang saya lihat, pengembang C # pertama berasal dari dunia C ++ (kasus saya), tetapi sekarang mereka beralih dari bahasa tingkat tinggi lainnya (saya tahu seorang pengembang yang menulis dalam JavaScript sebelumnya). Ini berarti orang semakin curiga dengan kode dan C / C + konstruksi yang tidak dikelola, lebih banyak ke Assembler.


Akibatnya, proyek mengandung kode yang semakin tidak aman dan kepercayaan pada platform API semakin tumbuh. Ini mudah untuk memeriksa apakah kita mencari kasus penggunaan stackalloc di repositori publik - mereka langka. Namun, mari kita ambil kode apa pun yang menggunakannya:


Kelas Interop.ReadDir
/src/mscorlib/shared/Interop/Unix/System.Native/Interop.ReadDir.cs


 unsafe { // s_readBufferSize is zero when the native implementation does not support reading into a buffer. byte* buffer = stackalloc byte[s_readBufferSize]; InternalDirectoryEntry temp; int ret = ReadDirR(dir.DangerousGetHandle(), buffer, s_readBufferSize, out temp); // We copy data into DirectoryEntry to ensure there are no dangling references. outputEntry = ret == 0 ? new DirectoryEntry() { InodeName = GetDirectoryEntryName(temp), InodeType = temp.InodeType } : default(DirectoryEntry); return ret; } 

Kita bisa melihat mengapa itu tidak populer. Hanya membaca kode ini dan tanyakan pada diri Anda apakah Anda mempercayainya. Saya kira jawabannya adalah 'Tidak'. Lalu, tanyakan pada diri Anda mengapa. Jelas: kita tidak hanya melihat kata Dangerous , yang menyarankan bahwa ada sesuatu yang salah, tetapi ada kata kunci dan byte* buffer = stackalloc byte[s_readBufferSize]; unsafe byte* buffer = stackalloc byte[s_readBufferSize]; line (khusus - byte* ) yang mengubah sikap kita. Ini adalah pemicu bagi Anda untuk berpikir: "Apakah tidak ada cara lain untuk melakukannya"? Jadi, mari kita masuk ke dalam psikoanalisis: mengapa Anda berpikir seperti itu? Di satu sisi, kami menggunakan konstruksi bahasa dan sintaks yang ditawarkan di sini jauh dari, misalnya, C ++ / CLI, yang memungkinkan apa saja (bahkan memasukkan kode Assembler murni). Di sisi lain, sintaks ini terlihat tidak biasa.


Masalah kedua yang dipikirkan pengembang secara implisit atau eksplisit adalah ketidakcocokan jenis string dan karakter. Meskipun, secara logis sebuah string adalah array karakter, tetapi Anda tidak dapat memberikan string ke char []: Anda hanya dapat membuat objek baru dan menyalin konten string ke array. Ketidakcocokan ini diperkenalkan untuk mengoptimalkan string dalam hal penyimpanan (tidak ada array hanya baca). Namun, masalah muncul ketika Anda mulai bekerja dengan file. Bagaimana cara membacanya? Sebagai string atau sebagai array? Jika Anda memilih array, Anda tidak dapat menggunakan beberapa metode yang dirancang untuk bekerja dengan string. Bagaimana dengan membaca sebagai string? Mungkin terlalu lama. Jika Anda perlu menguraikannya, parser apa yang harus Anda pilih untuk tipe data primitif: Anda tidak selalu ingin menguraikannya secara manual (integer, float, diberikan dalam format yang berbeda). Kami memiliki banyak algoritma terbukti yang melakukannya lebih cepat dan lebih efisien, bukan? Namun, algoritma seperti itu sering bekerja dengan string yang tidak mengandung apa pun kecuali tipe primitif itu sendiri. Jadi, ada dilema.


Masalah ketiga adalah bahwa data yang dibutuhkan oleh suatu algoritma jarang membuat irisan data yang solid dan kontinu dalam suatu bagian array yang dibaca dari beberapa sumber. Misalnya, dalam kasus file atau data dibaca dari soket, kami memiliki beberapa bagian yang sudah diproses oleh suatu algoritma, diikuti oleh bagian data yang harus diproses dengan metode kami, dan kemudian dengan belum memproses data. Idealnya, metode kami hanya menginginkan data yang dirancang untuk metode ini. Misalnya, metode yang mem-parsing bilangan bulat tidak akan senang dengan string yang berisi beberapa kata dengan angka yang diharapkan di suatu tempat di antara mereka. Metode ini menginginkan nomor dan tidak ada yang lain. Atau, jika kita melewatkan seluruh array, ada persyaratan untuk menunjukkan, misalnya, offset untuk angka dari awal array.


 int ParseInt(char[] input, int index) { while(char.IsDigit(input[index])) { // ... index++; } } 

Namun, pendekatan ini buruk, karena metode ini mendapatkan data yang tidak perlu. Dengan kata lain metode ini dipanggil untuk konteks yang tidak dirancang untuknya , dan harus menyelesaikan beberapa tugas eksternal. Ini desain yang buruk. Bagaimana cara menghindari masalah ini? Sebagai opsi, kita dapat menggunakan tipe ArraySegment<T> yang dapat memberikan akses ke bagian array:


 int ParseInt(IList<char>[] input) { while(char.IsDigit(input.Array[index])) { // ... index++; } } var arraySegment = new ArraySegment(array, from, length); var res = ParseInt((IList<char>)arraySegment); 

Namun, saya pikir ini terlalu banyak dalam hal logika dan penurunan kinerja. ArraySegment dirancang dengan buruk dan memperlambat akses ke elemen 7 kali lebih banyak dibandingkan dengan operasi yang sama dilakukan dengan array.


Jadi bagaimana kita mengatasi masalah ini? Bagaimana kami membuat pengembang kembali menggunakan kode yang tidak dikelola dan memberi mereka alat terpadu dan cepat untuk bekerja dengan sumber data yang heterogen: array, string, dan memori yang tidak dikelola. Penting untuk memberi mereka rasa percaya diri bahwa mereka tidak dapat melakukan kesalahan tanpa sadar. Itu perlu untuk memberi mereka instrumen yang tidak mengurangi tipe data asli dalam hal kinerja tetapi menyelesaikan masalah yang terdaftar. Span<T> dan Memory<T> adalah instrumen-instrumen ini.


Rentang <T>, ReadOnlySpan <T>


Jenis Span adalah instrumen untuk bekerja dengan data dalam bagian array data atau dengan subrange nilainya. Seperti dalam kasus array, ini memungkinkan membaca dan menulis ke elemen-elemen subrange ini, tetapi dengan satu kendala penting: Anda mendapatkan atau membuat Span<T> hanya untuk pekerjaan sementara dengan array, Hanya untuk memanggil sekelompok metode . Namun, untuk mendapatkan pemahaman umum, mari kita bandingkan jenis data yang dirancang untuk Span dan lihat kemungkinan skenario penggunaannya.


Jenis data pertama adalah array biasa. Array bekerja dengan Span dengan cara berikut:


  var array = new [] {1,2,3,4,5,6}; var span = new Span<int>(array, 1, 3); var position = span.BinarySearch(3); Console.WriteLine(span[position]); // -> 3 

Pada awalnya, kami membuat array data, seperti yang ditunjukkan oleh contoh ini. Selanjutnya, kita membuat Span (atau subset) yang merujuk ke array, dan membuat rentang nilai yang diinisialisasi sebelumnya dapat diakses oleh kode yang menggunakan array.


Di sini kita melihat fitur pertama dari tipe data ini yaitu kemampuan untuk membuat konteks tertentu. Mari kita kembangkan gagasan kita tentang konteks:


 void Main() { var array = new [] {'1','2','3','4','5','6'}; var span = new Span<char>(array, 1, 3); if(TryParseInt32(span, out var res)) { Console.WriteLine(res); } else { Console.WriteLine("Failed to parse"); } } public bool TryParseInt32(Span<char> input, out int result) { result = 0; for (int i = 0; i < input.Length; i++) { if(input[i] < '0' || input[i] > '9') return false; result = result * 10 + ((int)input[i] - '0'); } return true; } ----- 234 

Seperti yang kita lihat Span<T> menyediakan akses abstrak ke rentang memori baik untuk membaca dan menulis. Apa yang memberi kita? Jika kita ingat apa lagi yang bisa kita gunakan untuk Span , kita akan memikirkan sumber daya dan string yang tidak dikelola:


 // Managed array var array = new[] { '1', '2', '3', '4', '5', '6' }; var arrSpan = new Span<char>(array, 1, 3); if (TryParseInt32(arrSpan, out var res1)) { Console.WriteLine(res1); } // String var srcString = "123456"; var strSpan = srcString.AsSpan(); if (TryParseInt32(strSpan, out var res2)) { Console.WriteLine(res2); } // void * Span<char> buf = stackalloc char[6]; buf[0] = '1'; buf[1] = '2'; buf[2] = '3'; buf[3] = '4'; buf[4] = '5'; buf[5] = '6'; if (TryParseInt32(buf, out var res3)) { Console.WriteLine(res3); } ----- 234 234 234 

Itu berarti Span<T> adalah alat untuk menyatukan cara-cara bekerja dengan memori, baik yang dikelola maupun yang tidak dikelola. Ini memastikan keamanan saat bekerja dengan data tersebut selama Pengumpulan Sampah. Itu adalah jika rentang memori dengan sumber daya yang tidak dikelola mulai bergerak, itu akan aman.


Namun, haruskah kita begitu bersemangat? Bisakah kita mencapai ini lebih awal? Misalnya, dalam hal array yang dikelola tidak ada keraguan tentang hal itu: Anda hanya perlu membungkus array dalam satu kelas lagi (mis. [ArraySegment] yang sudah lama ada ( https://referencesource.microsoft.com/#mscorlib/system/ arraysegment.cs, 31 )) sehingga memberikan antarmuka yang sama dan hanya itu. Selain itu, Anda dapat melakukan hal yang sama dengan string - mereka memiliki metode yang diperlukan. Sekali lagi, Anda hanya perlu membungkus string dengan tipe yang sama dan memberikan metode untuk mengatasinya. Namun, untuk menyimpan string, buffer, dan array dalam satu jenis, Anda harus melakukan banyak hal dengan menyimpan referensi ke setiap varian yang mungkin ada dalam satu instance (dengan hanya satu varian aktif, jelas).


 public readonly ref struct OurSpan<T> { private T[] _array; private string _str; private T * _buffer; // ... } 

Atau, berdasarkan arsitektur Anda dapat membuat tiga jenis yang mengimplementasikan antarmuka yang seragam. Dengan demikian, tidak mungkin untuk membuat antarmuka yang seragam antara tipe data ini yang berbeda dari Span<T> dan mempertahankan kinerja maksimum.


Berikutnya, ada pertanyaan tentang apa yang dimaksud dengan ref struct dalam hal Span ? Ini adalah persis "struktur yang hanya ada di tumpukan" yang sering kita dengar selama wawancara kerja. Ini berarti tipe data ini dapat dialokasikan pada tumpukan saja dan tidak dapat pergi ke tumpukan. Inilah sebabnya mengapa Span , yang merupakan struktur ref, adalah tipe data konteks yang memungkinkan kerja metode tetapi bukan objek dalam memori. Itulah yang perlu kita dasari ketika mencoba memahaminya.


Sekarang kita dapat mendefinisikan tipe Span dan tipe ReadOnlySpan terkait:


Span adalah tipe data yang mengimplementasikan antarmuka yang seragam untuk bekerja dengan tipe array data yang heterogen dan memungkinkan melewati subset array ke metode sehingga kecepatan akses ke array asli akan konstan dan tertinggi terlepas dari kedalaman konteks.

Memang kalau kita punya kode suka


 public void Method1(Span<byte> buffer) { buffer[0] = 0; Method2(buffer.Slice(1,2)); } Method2(Span<byte> buffer) { buffer[0] = 0; Method3(buffer.Slice(1,1)); } Method3(Span<byte> buffer) { buffer[0] = 0; } 

kecepatan akses ke buffer asli akan menjadi yang tertinggi saat Anda bekerja dengan pointer yang dikelola dan bukan objek yang dikelola. Itu berarti Anda bekerja dengan jenis yang tidak aman di pembungkus terkelola, tetapi tidak dengan jenis .NET yang dikelola.


Bab ini diterjemahkan dari bahasa Rusia bersama oleh penulis dan penerjemah profesional . Anda dapat membantu kami dengan terjemahan dari bahasa Rusia atau Inggris ke bahasa lain, terutama ke bahasa Cina atau Jerman.

Juga, jika Anda ingin berterima kasih kepada kami, cara terbaik yang dapat Anda lakukan adalah memberi kami bintang di github atau untuk repositori garpu github / sidristij / dotnetbook .

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


All Articles