Tidak Aman. AsSpan: Rentang bagaimana cara mengganti pointer?


C# adalah bahasa yang sangat fleksibel. Di atasnya Anda dapat menulis tidak hanya aplikasi backend atau desktop. Saya menggunakan C# untuk bekerja dengan data ilmiah, yang memaksakan persyaratan tertentu pada alat yang tersedia dalam bahasa. Meskipun netcore mengambil agenda (mengingat bahwa setelah netstandard2.0 sebagian besar fitur dari kedua bahasa dan runtime tidak netframework untuk netframework ), saya terus bekerja dengan proyek-proyek warisan.


Pada artikel ini, saya menganggap satu aplikasi Span<T> tidak jelas (tapi mungkin diinginkan?) Dan perbedaan antara implementasi Span<T> dalam netframework dan netcore karena spesifikasi clr .


Penafian 1

Cuplikan kode dalam artikel ini sama sekali tidak dimaksudkan untuk digunakan dalam proyek dunia nyata.


Solusi yang diajukan untuk Masalah (yang dibuat-buat?) Itu lebih merupakan konsep bukti.
Bagaimanapun, dengan menerapkan ini dalam proyek Anda, Anda melakukan ini atas risiko dan risiko Anda sendiri.


Penafian 2

Saya benar-benar yakin bahwa di suatu tempat, dalam beberapa kasus, ini pasti akan menembak seseorang di lutut.


Tipe bypass keamanan dalam C# tidak mungkin mengarah ke sesuatu yang baik.


Untuk alasan yang jelas, saya tidak menguji kode ini dalam semua situasi yang mungkin, namun, hasil awal terlihat menjanjikan.


Mengapa saya perlu Span<T> ?


Spen memungkinkan Anda untuk bekerja dengan array tipe yang unmanaged dalam bentuk yang lebih nyaman, mengurangi jumlah alokasi yang diperlukan. Terlepas dari kenyataan bahwa span support di BCL netframework hampir sepenuhnya tidak ada, beberapa alat dapat diperoleh menggunakan System.Memory , System.Buffers dan System.Runtime.CompilerServices.Unsafe .
Penggunaan bentang dalam proyek lawas saya terbatas, namun, saya menemukan mereka penggunaan yang tidak terlihat, sambil meludahi keamanan jenis.
Apa aplikasi ini? Dalam proyek saya, saya bekerja dengan data yang diperoleh dari alat ilmiah. Ini adalah gambar yang, secara umum, adalah array dari T[] , di mana T adalah salah satu tipe primitif yang unmanaged , misalnya Int32 (alias int ). Untuk membuat serial gambar-gambar ini dengan benar ke disk, saya perlu mendukung format warisan yang sangat tidak nyaman, yang diusulkan pada tahun 1981 , dan sejak itu sedikit berubah. Masalah utama dari format ini adalah BigEndian . Jadi, untuk menulis (atau membaca) larik T[] tidak terkompresi, Anda perlu mengubah endianess dari setiap elemen. Tugas sepele.
Apa sajakah solusi yang jelas?


  1. Kita beralih ke array T[] , panggil BitConverter.GetBytes(T) , perluas beberapa byte ini, salin ke array target.
  2. Kami beralih ke array T[] , melakukan penipuan dari bentuk new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)}; (harus bekerja pada tipe byte ganda), tulis ke array target.
  3. * Tapi apakah T[] sebuah array? Elemen ada dalam satu baris, bukan? Jadi, Anda dapat melakukan semuanya, misalnya, Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int)); . Metode menyalin array ke array mengabaikan pemeriksaan jenis. Hanya perlu untuk tidak melewatkan batasan dan alokasi. Kami mencampur byte sebagai hasilnya.
  4. * Mereka mengatakan bahwa C# adalah (C++)++ . Oleh karena itu, aktifkan /unsafe , fixed(int* p = &intArr[0]) byte* bPtr = (byte*)p; dan sekarang Anda dapat menjalankan representasi byte dari array sumber, mengubah endianess dengan cepat dan menulis blok ke disk (menambahkan stackalloc byte[] atau ArrayPool<byte>.Shared untuk buffer perantara) tanpa mengalokasikan memori untuk array byte baru.

Tampaknya poin 4 memungkinkan Anda untuk menyelesaikan semua masalah, tetapi penggunaan eksplisit konteks yang unsafe dan bekerja dengan pointer agak berbeda. Lalu Span<T> datang membantu kami.


Span<T>


Span<T> harus secara teknis menyediakan alat untuk bekerja dengan plot memori hampir seperti bekerja melalui pointer, sambil menghilangkan kebutuhan untuk "memperbaiki" array dalam memori. Seperti pointer GC sadar dengan batas array. Semuanya baik dan aman.
Satu hal tetapi - terlepas dari kekayaan System.Runtime.CompilerServices.Unsafe , Span<T> dipaku untuk mengetik T Mengingat bahwa spen pada dasarnya adalah penunjuk panjang 1 +, bagaimana jika Anda menarik penunjuk Anda, mengonversinya ke jenis lain, menghitung ulang panjangnya dan membuat rentang baru? Untungnya, kami memiliki public Span<T>(void* pointer, int length) .
Mari kita menulis tes sederhana:


 [Test] public void Test() { void Flip(Span<byte> span) {/*   endianess */} Span<int> x = new [] {123}; Span<byte> y = DangerousCast<int, byte>(x); Assert.AreEqual(123, x[0]); Flip(y); Assert.AreNotEqual(123, x[0]); Flip(y); Assert.AreEqual(123, x[0]); } 

Pengembang yang lebih maju daripada saya harus segera menyadari apa yang salah di sini. Akankah tes gagal? Jawabannya, seperti biasanya terjadi, tergantung .
Dalam hal ini, itu tergantung terutama pada runtime. Pada netcore tes harus bekerja, tetapi pada netframework , bagaimana netframework .
Menariknya, jika Anda menghapus beberapa esai, tes mulai berfungsi dengan benar dalam 100% kasus.
Mari kita perbaiki.


Saya salah .


Jawaban yang benar: tergantung


Mengapa hasilnya tergantung ?
Mari kita hapus semua yang tidak perlu dan tulis kode di sini:


 private static void Main() => Check(); private static void Check() { Span<int> x = new[] {999, 123, 11, -100}; Span<byte> y = As<int, byte>(ref x); Console.WriteLine(@"FRAMEWORK_NAME"); Write(ref x); Write(ref y); Console.WriteLine(); Write<int, int>(ref x, "Span<int> [0]"); Write<byte, int>(ref y, "Span<byte>[0]"); Console.WriteLine(); Write<int, int>(ref Offset<int, object>(ref x[0], 1), "Span<int> [0] offset by size_t"); Write<byte, int>(ref Offset<byte, object>(ref y[0], 1), "Span<byte>[0] offset by size_t"); Console.WriteLine(); GC.Collect(0, GCCollectionMode.Forced, true, true); Write<int, int>(ref x, "Span<int> [0] after GC"); Write<byte, int>(ref y, "Span<byte>[0] after GC"); Console.WriteLine(); Write(ref x); Write(ref y); } 

Metode Write<T, U> menerima rentang tipe T , membaca alamat elemen pertama, dan membaca melalui pointer ini satu elemen dari tipe U Dengan kata lain, Write<int, int>(ref x) akan menampilkan alamat dalam memori + angka 999.
Normal Write mencetak array.
Sekarang tentang metode As<,> :


  private static unsafe Span<U> As<T, U>(ref Span<T> span) where T : unmanaged where U : unmanaged { fixed(T* ptr = span) return new Span<U>(ptr, span.Length * Unsafe.SizeOf<T>() / Unsafe.SizeOf<U>()); } 

Sintaks C# sekarang mendukung catatan kondisi fixed ini dengan memanggil metode Span<T>.GetPinnableReference() secara implisit.
Jalankan metode ini di netframework4.8 dalam mode x64 . Kami melihat apa yang terjadi:


 LEGACY [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|02|8C|00|00|2F|B0 999 Span<int> [0] 0x|00|00|02|8C|00|00|2F|B0 999 Span<byte>[0] 0x|00|00|02|8C|00|00|2F|B8 11 Span<int> [0] offset by size_t 0x|00|00|02|8C|00|00|2F|B8 11 Span<byte>[0] offset by size_t 0x|00|00|02|8C|00|00|2B|18 999 Span<int> [0] after GC 0x|00|00|02|8C|00|00|2F|B0 6750318 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 110, 0, 103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 

Awalnya, kedua bentang (terlepas dari jenis yang berbeda) berperilaku identik, dan Span<byte> , pada dasarnya, mewakili tampilan byte dari array asli. Apa yang kamu butuhkan
Oke, mari kita coba menggeser awal rentang ke ukuran satu IntPtr (atau 2 X int pada x64 ) dan membaca. Kami mendapatkan elemen ketiga dari array dan alamat yang benar. Dan kemudian kita akan mengumpulkan sampah ...


 GC.Collect(0, GCCollectionMode.Forced, true, true); 

Bendera terakhir dalam metode ini meminta GC memadatkan tumpukan. Setelah memanggil GC.Collect GC memindahkan array lokal asli. Span<int> mencerminkan perubahan ini, tetapi Span<byte> terus menunjuk ke alamat lama, di mana sekarang tidak jelas apa. Cara yang bagus untuk menembak diri sendiri dengan berlutut!


Sekarang mari kita lihat hasil dari fragmen kode yang sama persis disebut pada netcore3.0.100-preview8 .


 CORE [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<int> [0] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<byte>[0] 0x|00|00|01|F2|8F|BD|C6|98 11 Span<int> [0] offset by size_t 0x|00|00|01|F2|8F|BD|C6|98 11 Span<byte>[0] offset by size_t 0x|00|00|01|F2|8F|BD|BF|38 999 Span<int> [0] after GC 0x|00|00|01|F2|8F|BD|BF|38 999 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 

Semuanya bekerja, dan itu bekerja dengan stabil , sejauh yang saya bisa lihat. Setelah pemadatan, kedua spanyol mengubah penunjuknya. Hebat! Tetapi bagaimana sekarang membuatnya bekerja dalam proyek warisan?


Jit intrinsik


Saya benar-benar lupa bahwa dukungan untuk bentang diimplementasikan dalam netcore melalui intrinsik . Dengan kata lain, netcore dapat membuat pointer internal bahkan ke fragmen array dan memperbarui tautan dengan benar ketika GC memindahkannya. Dalam netframework , implementasi nuget dari span adalah penopang. Faktanya, kami memiliki dua spen yang berbeda: satu dibuat dari array dan melacak tautannya, yang kedua dari pointer dan tidak tahu apa yang ditunjukkannya. Setelah memindahkan array asli, pointer span terus menunjuk ke tempat pointer melewati konstruktornya menunjuk. Sebagai perbandingan, ini adalah contoh implementasi span di netcore :


 readonly ref struct Span<T> where T : unmanaged { private readonly ByReference<T> _pointer; //  -   private readonly int _length; } 

dan di netframework :


 readonly ref struct Span<T> where T : unmanaged { private readonly Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; } 

_pinnable berisi referensi ke array, jika seseorang dilewatkan ke konstruktor, _byteOffset berisi pergeseran (bahkan rentang seluruh array memiliki beberapa pergeseran non-nol terkait dengan cara array diwakili dalam memori, mungkin ). Jika Anda melewatkan void* pointer ke konstruktor, itu hanya dikonversi ke absolut _byteOffset . Rentang akan dipaku erat ke area memori, dan semua metode contoh penuh dengan kondisi seperti if(_pinnable is null) {/* */} else {/* _pinnable */} . Apa yang harus dilakukan dalam situasi seperti itu?


Bagaimana melakukannya tidak sepadan, tetapi saya masih melakukannya


Bagian ini dikhususkan untuk berbagai implementasi yang didukung oleh netframework , yang memungkinkan netframework Span<T> -> Span<U> , menjaga semua tautan yang diperlukan.
Saya memperingatkan Anda: ini adalah zona pemrograman abnormal dengan kemungkinan kesalahan mendasar dan Perilaku Tidak Terdefinisi pada akhirnya


Metode 1: Naif


Seperti yang ditunjukkan contoh, konversi pointer tidak akan memberikan hasil yang diinginkan pada netframework . Kami membutuhkan nilai _pinnable . Oke, kami akan mengungkap refleksi dengan menarik keluar bidang pribadi (sangat buruk dan tidak selalu mungkin), kami akan menuliskannya di spen baru, kami akan senang. Hanya ada satu masalah kecil : spen adalah sebuah ref struct , itu tidak bisa menjadi argumen umum, juga tidak dapat dikemas menjadi object . Metode refleksi standar akan membutuhkan, dengan satu atau lain cara, untuk mendorong rentang ke tipe referensi. Saya tidak menemukan cara sederhana (bahkan mempertimbangkan refleksi di bidang pribadi).


Metode 2: Kita perlu lebih dalam


Semuanya telah dilakukan sebelum saya ( [1] , [2] , [3] ). Spen adalah struktur, terlepas dari T tiga bidang menempati jumlah memori yang sama ( pada arsitektur yang sama ). Bagaimana jika [FieldOffset(0)] ? Tidak lebih cepat dikatakan daripada dilakukan.


 [StructLayout(LayoutKind.Explicit)] ref struct Exchange<T, U> where T : unmanaged where U : unmanaged { [FieldOffset(0)] public Span<T> Span_1; [FieldOffset(0)] public Span<U> Span_2; } 

Tetapi ketika Anda memulai program (atau lebih tepatnya, ketika mencoba menggunakan tipe), TypeLoadException bertemu dengan TypeLoadException - generik tidak boleh LayoutKind.Explicit . Oke, itu tidak masalah, ayo jalan yang sulit:


 [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Span<byte> ByteSpan; [FieldOffset(0)] public Span<sbyte> SByteSpan; [FieldOffset(0)] public Span<ushort> UShortSpan; [FieldOffset(0)] public Span<short> ShortSpan; [FieldOffset(0)] public Span<uint> UIntSpan; [FieldOffset(0)] public Span<int> IntSpan; [FieldOffset(0)] public Span<ulong> ULongSpan; [FieldOffset(0)] public Span<long> LongSpan; [FieldOffset(0)] public Span<float> FloatSpan; [FieldOffset(0)] public Span<double> DoubleSpan; [FieldOffset(0)] public Span<char> CharSpan; } 

Sekarang Anda bisa melakukan ini:


 private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; return exchange.ByteSpan; } 

Metode ini bekerja hanya dengan satu masalah - bidang _length disalin sebagaimana _length , jadi ketika casting int -> byte rentang byte 4 kali lebih kecil dari array nyata.
Tidak masalah:


 [StructLayout(LayoutKind.Sequential)] public ref struct Raw { public object Pinnable; public IntPtr Pointer; public int Length; } [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { /* */ [FieldOffset(0)] public Raw RawView; } 

Sekarang melalui RawView Anda dapat mengakses setiap bidang rentang individual.


 private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; var exchange2 = new Exchange() { RawView = new Raw() { Pinnable = exchange.RawView.Pinnable, Pointer = exchange.RawView.Pointer, Length = exchange.RawView.Length * sizeof<int> / sizeof<byte> } }; return exchange2.ByteSpan; } 

Dan itu berfungsi sebagaimana mestinya , jika Anda mengabaikan penggunaan trik kotor. Minus - versi generik dari konverter tidak dapat dibuat, Anda harus puas dengan tipe yang telah ditentukan.


Metode 3: Gila


Seperti programmer normal, saya suka mengotomatiskan hal-hal. Kebutuhan untuk menulis konverter untuk setiap pasangan jenis yang tidak unmanaged tidak menyenangkan saya. Solusi apa yang bisa ditawarkan? Itu benar, dapatkan CLR untuk menulis kode untuk Anda .


Bagaimana cara mencapai ini? Ada berbagai cara, ada artikel . Singkatnya, prosesnya terlihat seperti ini:
Buat build builder -> create builder modul -> build a type -> {Fields, Methods, etc.} -> pada output kita mendapatkan instance Type .
Untuk memahami persis seperti apa bentuknya (ini adalah ref struct ), kami menggunakan alat apa pun dari jenis ildasm . Dalam kasus saya, itu dotPeek .
Membuat pembuat tipe terlihat seperti ini:


 var typeBuilder = _mBuilder.DefineType($"Generated_{typeof(T).Name}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.ExplicitLayout // <-    | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, typeof(ValueType)); 

Sekarang bidangnya. Karena kita tidak dapat secara langsung menyalin Span<T> ke Span<U> karena perbedaan panjangnya, kita perlu membuat dua jenis masing-masing pemain


 [StructLayout(LayoutKind.Explicit)] ref struct Generated_Int32 { [FieldOffset(0)] public Span<Int32> Span; [FieldOffset(0)] public Raw Raw; } 

Di sini, Raw kita dapat mendeklarasikan dengan tangan kita dan menggunakan kembali. Jangan lupa tentang IsByRefLikeAttribute . Dengan bidang, semuanya sederhana:


 var spanField = typeBuilder.DefineField("Span", typeof(Span<T>), FieldAttributes.Private); spanField.SetOffset(0); var rawField = typeBuilder.DefineField("Raw", typeof(Raw), FieldAttributes.Private); rawField.SetOffset(0); 

Itu saja, tipe paling sederhana sudah siap. Sekarang cache modul assembly. Jenis khusus di-cache, misalnya, dalam kamus ( T -> Generated_{nameof(T)} ). Kami membuat pembungkus yang, menurut dua jenis TOut dan TOut menghasilkan dua jenis pembantu dan melakukan operasi yang diperlukan pada bentang. Ada satu tapi. Seperti dalam kasus refleksi, hampir tidak mungkin untuk menggunakannya pada bentang (atau pada ref struct lainnya). Atau saya tidak menemukan solusi yang sederhana . Bagaimana menjadi?


Delegasi untuk menyelamatkan


Metode refleksi biasanya terlihat seperti ini:


  object Invoke(this MethodInfo mi, object @this, object[] otherArgs) 

Mereka tidak membawa informasi tentang jenis, jadi jika tinju (= kemasan) dapat Anda terima, tidak ada masalah.
Dalam kasus kami, @this dan otherArgs harus mengandung ref struct , yang saya tidak bisa menyiasati.
Namun, ada cara yang lebih sederhana. Mari kita bayangkan bahwa suatu tipe memiliki metode pengambil dan penyetel (bukan properti, tetapi metode sederhana yang dibuat secara manual).
Sebagai contoh:


 void Generated_Int32.SetSpan(Span<Int32> span) => this.Span = span; 

Selain metode, kita dapat mendeklarasikan tipe delegasi (secara eksplisit dalam kode):


 delegate void SpanSetterDelegate<T>(Span<T> span) where T : unmanaged; 

Kita harus melakukan ini karena tindakan standar harus memiliki tanda tangan Action<Span<T>> , tetapi adegan tidak dapat digunakan sebagai argumen umum. SpanSetterDelegate , bagaimanapun, adalah delegasi yang benar-benar valid.
Buat delegasi yang diperlukan. Untuk melakukan ini, lakukan manipulasi standar:


 var mi = type.GetMethod("Method_Name"); // ,    public & instance var spanSetter = (SpanSetterDelegate<T>) mi.CreateDelegate(typeof(SpanSetterDelegate<T>), @this); 

Sekarang spanSetter dapat digunakan sebagai, misalnya, spanSetter(Span<T>.Empty); . Adapun @this 2 , ini adalah turunan dari tipe dinamis kami, dibuat, tentu saja, melalui Activator.CreateInstance(type) , karena struktur memiliki konstruktor default tanpa argumen.


Jadi, perbatasan terakhir - kita perlu secara dinamis menghasilkan metode.


2 Anda mungkin melihat ada sesuatu yang salah di sini - Activator.CreateInstance() mengemas instance ref struct . Lihat akhir bagian selanjutnya.


Temui Reflection.Emit


Saya pikir metode dapat dihasilkan menggunakan Expression , sebagai tubuh getter / setter sepele kami terdiri dari beberapa ekspresi. Saya memilih pendekatan yang berbeda dan lebih langsung.


Jika Anda melihat kode IL dari pengambil yang sepele, Anda dapat melihat sesuatu seperti ( Debug , X86 , netframework4.8 )


 nop ldarg.0 ldfld /* - */ stloc.0 br.s /*  */ ldloc.0 ret 

Ada banyak tempat untuk berhenti dan debug.
Dalam versi rilis, hanya yang paling penting yang tersisa:


 ldarg.0 ldfld /* - */ ret 

Argumen nol metode instance adalah ... this . Jadi, yang berikut ini ditulis dalam IL :
1) Unduh this
2) Masukkan nilai bidang
3) Bawa kembali


Hanya ya? Reflection.Emit memiliki kelebihan khusus yang dibutuhkan, selain kode op, juga parameter deskriptor bidang. Sama seperti yang kami terima sebelumnya, misalnya spanField .


 var getSpan = type.DefineMethod("GetSpan", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.Standard, typeof(Span<T>), Array.Empty<Type>()); gen = getSpan.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldfld, spanField); gen.Emit(OpCodes.Ret); 

Untuk penyetel, ini sedikit lebih rumit, Anda perlu memuat ini di stack, memuat argumen pertama dari fungsi, lalu memanggil instruksi tulis di bidang dan tidak mengembalikan apa pun:


 ldarg.0 ldarg.1 stfld /*   */ ret 

Setelah melakukan prosedur ini untuk bidang Raw , mendeklarasikan delegasi yang diperlukan (atau menggunakan yang standar), kami mendapatkan tipe dinamis dan empat metode pengakses dari mana delegasi generik yang benar dihasilkan.


Kami menulis kelas pembungkus yang, menggunakan dua parameter generik ( TOut , TOut ), menerima instance tipe Type yang mereferensikan tipe dinamis (cache) yang sesuai, setelah itu ia membuat satu objek dari setiap tipe dan menghasilkan empat delegasi generik, yaitu


  1. void SetSpan(Span<TIn> span) untuk menulis rentang sumber ke struktur
  2. Raw GetRaw() untuk membaca konten span sebagai struktur Raw
  3. void SetRaw(Raw raw) untuk menulis struktur Raw dimodifikasi ke objek kedua
  4. Span<TOut> GetSpan() untuk mengembalikan rentang jenis yang diinginkan dengan bidang yang ditetapkan dan dihitung ulang dengan benar.

Menariknya, instance tipe dinamis perlu dibuat sekali. Saat membuat delegasi, referensi ke objek-objek ini dilewatkan sebagai parameter @this . Ini adalah pelanggaran aturan. Activator.CreateInstance mengembalikan object . Rupanya ini disebabkan oleh fakta bahwa tipe dinamis itu sendiri tidak menghasilkan ref type.IsByRef ( type.IsByRef Like == false ), tetapi dimungkinkan untuk membuat bidang ref like. Rupanya, pembatasan semacam itu ada dalam bahasa itu, tetapi CLR mencernanya. Mungkin di sinilah lutut akan ditembak jika digunakan tidak standar. 3


Jadi, kita mendapatkan instance dari tipe generik yang berisi empat delegasi dan dua referensi implisit ke instance kelas dinamis. Delegasi dan struktur dapat digunakan kembali saat melakukan kasta yang sama dalam satu baris. Untuk meningkatkan kinerja, kami cache lagi (sudah konverter tipe) untuk sepasang (TIn, TOut) -> Generator<TIn, TOut> .


Stroke adalah yang terakhir: kami berikan tipe, Span<TIn> -> Span<TOut>


 public Span<TOut> Cast(Span<TIn> span) { //      if (span.IsEmpty) return Span<TOut>.Empty; // Caller   ,       if (span.Length * Unsafe.SizeOf<TIn>() % Unsafe.SizeOf<TOut>() != 0) throw new InvalidOperationException(); //      // Span<TIn> _input.Span = span; _spanSetter(span); //  Raw // Raw raw = _input.Raw; var raw = _rawGetter(); var newRaw = new Raw() { Pinnable = raw.Pinnable, //    Pinnable Pointer = raw.Pointer, //   Length = raw.Length * Unsafe.SizeOf<TIn>() / Unsafe.SizeOf<TOut>() //   }; //   Raw    // Raw _output.Raw = newRaw; _rawSetter(newRaw); //     // Span<TOut> _output.Span return _spanGetter(); } 

Kesimpulan


Terkadang - demi minat olahraga - Anda dapat melewati beberapa batasan bahasa dan menerapkan fungsi yang tidak standar. Tentu saja, dengan risiko dan risiko Anda sendiri. Perlu dicatat bahwa metode dinamis memungkinkan Anda untuk sepenuhnya meninggalkan pointer dan konteks yang unsafe / fixed , yang bisa menjadi bonus. Kelemahan yang jelas adalah perlunya refleksi dan generasi jenis.


Bagi yang sudah membaca sampai akhir.


Hasil Benchmark Naif

Dan seberapa cepat semuanya?
Saya membandingkan kecepatan kasta dalam skenario bodoh yang tidak mencerminkan penggunaan aktual / potensial kasta dan rentang tersebut, tetapi setidaknya memberikan gambaran tentang kecepatan.


  1. Cast_Explicitmenggunakan konversi melalui tipe yang dinyatakan secara eksplisit, seperti dalam Metode 2 . Setiap kasta membutuhkan alokasi dua struktur kecil dan akses ke ladang;
  2. Cast_ILmengimplementasikan Metode 3 , tetapi setiap kali membuat instance lagi Generator<TIn, TOut>, yang mengarah ke pencarian konstan dalam kamus, setelah pass pertama menghasilkan semua jenis;
  3. Cast_IL_Cachedcache instance konverter langsung Generator<TIn, TOut>, itulah sebabnya ternyata menjadi lebih cepat rata-rata, karena seluruh kasta bermuara pada panggilan empat delegasi;
  4. Buffer , , . .

int[N] N/2 .


, , . , . , , . , unmanaged .


 BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i7-2700K CPU 3.50GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Job=Clr Runtime=Clr InvocationCount=1 UnrollFactor=1 

MethodNMeanErrorStdDevMedianRatioRatioSD
Cast_Explicit100362.2 ns18.0967 ns52.7888 ns400.0 ns1.000.00
Cast_IL1001,237.9 ns28.5954 ns67.4027 ns1,200.0 ns3.470.51
Cast_IL_Cached100522.8 ns25.2640 ns71.2576 ns500.0 ns1.460.27
Buffer100300.0 ns0.0000 ns0.0000 ns300.0 ns0.780.11
Cast_Explicit10002,628.6 ns54.0688 ns64.3650 ns2,600.0 ns1.000.00
Cast_IL10003,216.7 ns49.8568 ns38.9249 ns3,200.0 ns1.210.03
Cast_IL_Cached10002,484.6 ns44.9717 ns37.5534 ns2,500.0 ns0.940.02
Buffer10002,055.6 ns43.9695 ns73.4631 ns2,000.0 ns0.780.03
Cast_Explicit10000002,515,157.1 ns11,809.8538 ns10,469.1278 ns2,516,050.0 ns1.000.00
Cast_IL1.000.0002,263,826.7 ns23,724.4930 ns22,191.9054 ns2,262,000.0 ns0.900.01
Cast_IL_Cached1.000.0002,265,186.7 ns19,505.5913 ns18,245.5422 ns2,266,300.0 ns0.900.01
Buffer1.000.0001,959,547.8 ns39,175.7435 ns49,544.7719 ns1,959,200.0 ns0.780.02
Cast_Explicit100000000255,751,392.9 ns2,595,107.7066 ns2,300,495.3873 ns255,298,950.0 ns1.000.00
Cast_IL100000000228,709,457.1 ns527,430.9293 ns467,553.7809 ns228,864,100.0 ns0.890.01
Cast_IL_Cached100000000227,966,553.8 ns355,027.3545 ns296,463.9203 ns227,903,600.0 ns0.890.01
Buffer100000000213,216,776.9 ns1,198,565.1142 ns1,000,856.1536 ns213,517,800.0 ns0.830.01

Acknowledgements

JetBrains ( :-)) R# VS standalone- dotPeek , . BenchmarkDotNet BenchmarkDotNet, youtube- NDC Conferences DotNext , , .


PS


3 , ref , , . ( ) . ref structs,


 static Raw Generated_Int32.GetRaw(Span<int> span) { var inst = new Generated_Int32() { Span = span }; return inst.Raw; } 

, Reflection.Emit . , ILGenerator.DeclareLocal .


 static Span<int> Generated_Int32.GetSpan(Raw raw); 


 delegate Raw GetRaw<T>(Span<T> span) where T : unmanaged; delegate Span<T> GetSpan<T>(Raw raw) where T : unmanaged; 

, , ref — . Karena ,


 var getter = type.GetMethod(@"GetRaw", BindingFlags.Static | BindingFlags.Public).CreateDelegate(typeof(GetRaw<T>), null) as GetRaw<T>; 


 Raw raw = getter(Span<TIn>.Empty); Raw newRaw = convert(raw); Span<TOut> = setter(newRaw); 

UPD01:

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


All Articles