Mengapa TypedReference berada di belakang layar? Ini sangat cepat dan aman ... hampir ajaib!

128

Peringatan: Pertanyaan ini agak sesat ... programmer agama selalu taat pada praktik yang baik, tolong jangan membacanya. :)

Adakah yang tahu mengapa menggunakan TypedReference sangat tidak disarankan (secara implisit, karena kurangnya dokumentasi)?

Saya telah menemukan kegunaan besar untuk itu, seperti ketika melewati parameter generik melalui fungsi yang seharusnya tidak generik (saat menggunakan objectmungkin berlebihan atau lambat, jika Anda membutuhkan tipe nilai), untuk saat Anda membutuhkan pointer buram, atau ketika Anda perlu mengakses elemen array dengan cepat, yang spesifikasinya Anda temukan saat runtime (menggunakan Array.InternalGetReference). Karena CLR bahkan tidak mengizinkan penggunaan yang salah dari jenis ini, mengapa itu tidak disarankan? Sepertinya tidak aman atau apa pun ...


Kegunaan lain yang saya temukan untuk TypedReference:

"Mengkhususkan" obat generik dalam C # (ini aman untuk jenis):

static void foo<T>(ref T value)
{
    //This is the ONLY way to treat value as int, without boxing/unboxing objects
    if (value is int)
    { __refvalue(__makeref(value), int) = 1; }
    else { value = default(T); }
}

Menulis kode yang berfungsi dengan pointer umum (ini sangat tidak aman jika disalahgunakan, tetapi cepat dan aman jika digunakan dengan benar):

//This bypasses the restriction that you can't have a pointer to T,
//letting you write very high-performance generic code.
//It's dangerous if you don't know what you're doing, but very worth if you do.
static T Read<T>(IntPtr address)
{
    var obj = default(T);
    var tr = __makeref(obj);

    //This is equivalent to shooting yourself in the foot
    //but it's the only high-perf solution in some cases
    //it sets the first field of the TypedReference (which is a pointer)
    //to the address you give it, then it dereferences the value.
    //Better be 10000% sure that your type T is unmanaged/blittable...
    unsafe { *(IntPtr*)(&tr) = address; }

    return __refvalue(tr, T);
}

Menulis versi metodesizeof instruksi, yang kadang-kadang berguna:

static class ArrayOfTwoElements<T> { static readonly Value = new T[2]; }

static uint SizeOf<T>()
{
    unsafe 
    {
        TypedReference
            elem1 = __makeref(ArrayOfTwoElements<T>.Value[0] ),
            elem2 = __makeref(ArrayOfTwoElements<T>.Value[1] );
        unsafe
        { return (uint)((byte*)*(IntPtr*)(&elem2) - (byte*)*(IntPtr*)(&elem1)); }
    }
}

Menulis metode yang melewati parameter "status" yang ingin menghindari tinju:

static void call(Action<int, TypedReference> action, TypedReference state)
{
    //Note: I could've said "object" instead of "TypedReference",
    //but if I had, then the user would've had to box any value types
    try
    {
        action(0, state);
    }
    finally { /*Do any cleanup needed*/ }
}

Jadi mengapa penggunaan seperti ini "tidak disarankan" (karena kurangnya dokumentasi)? Adakah alasan keamanan tertentu? Tampaknya sangat aman dan dapat diverifikasi jika tidak dicampur dengan pointer (yang sebenarnya tidak aman atau dapat diverifikasi) ...


Memperbarui:

Kode contoh untuk menunjukkan bahwa, memang, TypedReferencebisa dua kali lebih cepat (atau lebih):

using System;
using System.Collections.Generic;
static class Program
{
    static void Set1<T>(T[] a, int i, int v)
    { __refvalue(__makeref(a[i]), int) = v; }

    static void Set2<T>(T[] a, int i, int v)
    { a[i] = (T)(object)v; }

    static void Main(string[] args)
    {
        var root = new List<object>();
        var rand = new Random();
        for (int i = 0; i < 1024; i++)
        { root.Add(new byte[rand.Next(1024 * 64)]); }
        //The above code is to put just a bit of pressure on the GC

        var arr = new int[5];
        int start;
        const int COUNT = 40000000;

        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set1(arr, 0, i); }
        Console.WriteLine("Using TypedReference:  {0} ticks",
                          Environment.TickCount - start);
        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set2(arr, 0, i); }
        Console.WriteLine("Using boxing/unboxing: {0} ticks",
                          Environment.TickCount - start);

        //Output Using TypedReference:  156 ticks
        //Output Using boxing/unboxing: 484 ticks
    }
}

(Sunting: Saya mengedit patokan di atas, karena versi terakhir dari pos menggunakan versi debug kode [saya lupa mengubahnya untuk melepaskan], dan tidak memberi tekanan pada GC. Versi ini sedikit lebih realistis, dan pada sistem saya, ini lebih dari tiga kali lebih cepat dengan TypedReferencerata-rata.)

pengguna541686
sumber
Ketika saya menjalankan contoh Anda, saya mendapatkan hasil yang sangat berbeda. TypedReference: 203 ticks, boxing/unboxing: 31 ticks. Tidak peduli apa yang saya coba (termasuk berbagai cara untuk melakukan penghitungan waktu) tinju / unboxing masih lebih cepat di sistem saya.
Seph
1
@Seph: Saya baru saja melihat komentar Anda. Itu sangat menarik - sepertinya lebih cepat pada x64, tetapi lebih lambat pada x86. Aneh ...
user541686
1
Saya baru saja menguji kode benchmark pada mesin x64 saya di bawah .NET 4.5. Saya mengganti Environment.TickCount dengan Diagnostics.Stopwatch dan pergi dengan ms bukannya ticks. Saya menjalankan setiap build (x86, 64, Any) tiga kali. Yang terbaik dari tiga hasil adalah sebagai follws: x86: 205 / 27ms (hasil yang sama untuk 2/3 run pada build ini) x64: 218 / 109ms Any: 205 / 27ms (hasil yang sama untuk 2/3 run pada build ini) In -semua kotak case / unboxing lebih cepat.
kornman00
2
Pengukuran kecepatan aneh mungkin dikaitkan dengan dua fakta ini: * (T) (objek) v TIDAK benar-benar membuat alokasi tumpukan. Di .NET 4+ ini dioptimalkan jauh. Tidak ada alokasi pada jalur ini, dan sangat cepat. * Menggunakan makeref membutuhkan variabel yang benar-benar dialokasikan pada stack (sementara metode agak-kotak mungkin mengoptimalkannya ke register). Juga, dengan melihat timing, saya berasumsi bahwa itu merusak inlining bahkan dengan flag force-inline. Jadi, agak-kotak diuraikan dan didaftarkan, sementara makeref membuat panggilan fungsi dan mengoperasikan stack
hypersw
1
Untuk melihat keuntungan dari casting typeref, membuatnya kurang sepele. Misalnya casting tipe yang mendasarinya ke tipe enum ( int-> DockStyle). Kotak ini nyata, dan hampir sepuluh kali lebih lambat.
hypersw

Jawaban:

42

Jawaban singkat: portabilitas .

Sementara __arglist, __makeref, dan __refvalueyang ekstensi bahasa dan tak berdokumen di C # Language Specification, konstruksi digunakan untuk menerapkannya di bawah tenda ( varargkonvensi pemanggilan, TypedReferencejenis, arglist, refanytype, mkanyref, dan refanyvalpetunjuk) yang sempurna didokumentasikan dalam CLI Keterangan (ECMA-335) di yang perpustakaan vararg .

Didefinisikan di Perpustakaan Vararg membuatnya sangat jelas bahwa mereka terutama dimaksudkan untuk mendukung daftar argumen panjang variabel dan tidak banyak lagi. Daftar argumen-variabel memiliki sedikit kegunaan dalam platform yang tidak perlu berinteraksi dengan kode C eksternal yang menggunakan varargs. Untuk alasan ini, pustaka Varargs bukan bagian dari profil CLI mana pun. Implementasi CLI yang sah dapat memilih untuk tidak mendukung pustaka Varargs karena itu tidak termasuk dalam profil Kernel CLI:

4.1.6 Vararg

Set fitur vararg mendukung daftar argumen panjang variabel dan pointer yang diketik dengan runtime.

Jika dihilangkan: Setiap upaya untuk merujuk metode dengan varargkonvensi pemanggilan atau pengkodean tanda tangan yang terkait dengan metode vararg (lihat Partisi II) harus membuang System.NotImplementedExceptionpengecualian. Metode menggunakan petunjuk CIL arglist, refanytype, mkrefany, dan refanyvalakan melemparkan System.NotImplementedExceptionpengecualian. Waktu yang tepat dari pengecualian tidak ditentukan. Jenis yang System.TypedReferencetidak perlu didefinisikan.

Perbarui (balas ke GetValueDirect komentar):

FieldInfo.GetValueDirectadalah FieldInfo.SetValueDirectyang tidak bagian dari Base Class Library. Perhatikan bahwa ada perbedaan antara .NET Framework Class Library dan Base Class Library. BCL adalah satu-satunya hal yang diperlukan untuk implementasi CLI / C # yang sesuai dan didokumentasikan dalam ECMA TR / 84 . (Sebenarnya, FieldInfoitu sendiri adalah bagian dari perpustakaan Reflection dan itu tidak termasuk dalam profil Kernel CLI juga).

Segera setelah Anda menggunakan metode di luar BCL, Anda memberikan sedikit portabilitas (dan ini menjadi semakin penting dengan munculnya implementasi CLI non-.NET seperti Silverlight dan MonoTouch). Bahkan jika suatu implementasi ingin meningkatkan kompatibilitas dengan Microsoft .NET Framework Class Library, ia dapat dengan mudah menyediakan GetValueDirectdan SetValueDirectmengambil TypedReferencetanpa membuat TypedReferencepenanganan khusus oleh runtime (pada dasarnya, membuat mereka setara dengan objectrekan - rekan mereka tanpa manfaat kinerja).

Jika mereka mendokumentasikannya dalam C #, itu akan memiliki setidaknya beberapa implikasi:

  1. Seperti fitur apa pun, ini dapat menjadi penghalang bagi fitur baru, terutama karena fitur ini tidak benar-benar cocok dengan desain C # dan memerlukan ekstensi sintaksis yang aneh dan penyerahan khusus suatu tipe oleh runtime.
  2. Semua implementasi C # harus entah bagaimana mengimplementasikan fitur ini dan itu tidak selalu sepele / mungkin untuk implementasi C # yang tidak berjalan di atas CLI sama sekali atau berjalan di atas CLI tanpa Varargs.
Mehrdad Afshari
sumber
4
Argumen yang bagus untuk portabilitas, +1. Tapi bagaimana dengan FieldInfo.GetValueDirectdan FieldInfo.SetValueDirect? Mereka adalah bagian dari BCL, dan untuk menggunakannya Anda perlu TypedReference , jadi bukankah itu pada dasarnya memaksa TypedReferenceuntuk selalu didefinisikan, terlepas dari spesifikasi bahasa? (Juga, catatan lain: Sekalipun kata kunci tidak ada, selama instruksinya ada, Anda masih bisa mengaksesnya dengan metode yang memancarkan secara dinamis ... jadi selama platform Anda berhenti dengan pustaka C, Anda dapat menggunakan ini, apakah C # memiliki kata kunci atau tidak.)
user541686
Oh, dan masalah lain: Meskipun tidak portabel, mengapa mereka tidak mendokumentasikan kata kunci? Paling tidak, itu perlu saat berinteraksi dengan C varargs, jadi setidaknya mereka bisa menyebutkannya?
user541686
@Mehrdad: Hah, itu menarik. Saya kira saya selalu berasumsi bahwa file dalam folder BCL dari sumber .NET adalah bagian dari BCL, tidak pernah benar-benar memperhatikan bagian standardisasi ECMA. Ini cukup meyakinkan ... kecuali satu hal kecil: bukankah tidak ada gunanya untuk bahkan menyertakan fitur (opsional) dalam spesifikasi CLI, jika tidak ada dokumentasi tentang cara menggunakannya di mana saja? (Akan masuk akal jika TypedReferencedidokumentasikan hanya untuk satu bahasa - katakanlah, dikelola C ++ - tetapi jika tidak ada bahasa mendokumentasikannya dan jadi jika tidak ada yang benar-benar dapat menggunakannya, lalu mengapa bahkan repot mendefinisikan fitur?)
user541686
@Mehrdad Saya menduga motivasi utama adalah kebutuhan untuk fitur ini secara internal untuk interop ( misalnya [DllImport("...")] void Foo(__arglist); ) dan mereka menerapkannya dalam C # untuk penggunaan mereka sendiri. Desain CLI dipengaruhi oleh banyak bahasa (anotasi "The Common Language Infrastructure Annotated Standard" menunjukkan fakta ini.) Menjadi runtime yang cocok untuk sebanyak mungkin bahasa, termasuk yang tidak terduga, jelas telah menjadi tujuan desain (oleh karena itu, nama) dan ini adalah fitur yang, misalnya, implementasi C yang dikelola secara hipotesis mungkin dapat diuntungkan.
Mehrdad Afshari
@Mehrdad: Ah ... ya, itu alasan yang cukup meyakinkan. Terima kasih!
user541686
15

Yah, saya bukan Eric Lippert, jadi saya tidak dapat berbicara langsung tentang motivasi Microsoft, tetapi jika saya berani menebak, saya akan mengatakan itu TypedReferencedkk. tidak didokumentasikan dengan baik karena, terus terang, Anda tidak membutuhkannya.

Setiap penggunaan yang Anda sebutkan untuk fitur-fitur ini dapat diselesaikan tanpa fitur-fitur tersebut, meskipun pada penalti kinerja dalam beberapa kasus. Tetapi C # (dan .NET secara umum) tidak dirancang untuk menjadi bahasa kinerja tinggi. (Saya menduga bahwa "lebih cepat dari Jawa" adalah tujuan kinerja.)

Itu bukan untuk mengatakan bahwa pertimbangan kinerja tertentu belum diberikan. Memang, fitur seperti pointer,, stackallocdan fungsi kerangka kerja tertentu yang dioptimalkan ada sebagian besar untuk meningkatkan kinerja dalam situasi tertentu.

Obat generik, yang menurut saya memiliki manfaat utama dari keamanan tipe, juga meningkatkan kinerja seperti halnya TypedReferencemenghindari tinju dan unboxing. Bahkan, saya bertanya-tanya mengapa Anda lebih suka ini:

static void call(Action<int, TypedReference> action, TypedReference state){
    action(0, state);
}

untuk ini:

static void call<T>(Action<int, T> action, T state){
    action(0, state);
}

Pertukarannya, seperti yang saya lihat, adalah bahwa yang pertama membutuhkan JIT yang lebih sedikit (dan, yang mengikuti, lebih sedikit memori), sedangkan yang terakhir lebih akrab dan, saya akan berasumsi, sedikit lebih cepat (dengan menghindari penunjuk dereferencing).

Saya akan menelepon TypedReferencedan detail penerapan teman. Anda telah menunjukkan beberapa kegunaan yang rapi untuk mereka, dan saya pikir mereka layak untuk ditelusuri, tetapi peringatan yang biasanya bergantung pada detail implementasi berlaku — versi berikutnya dapat memecahkan kode Anda.

P Ayah
sumber
4
Hah ... "kamu tidak membutuhkannya" - Aku seharusnya sudah melihat itu. :-) Itu benar tetapi itu juga tidak benar. Apa yang Anda definisikan sebagai "kebutuhan"? Apakah metode ekstensi benar-benar "diperlukan", misalnya? Mengenai pertanyaan Anda tentang penggunaan obat generik di call(): Itu karena kodenya tidak selalu begitu kohesif - saya merujuk lebih ke contoh yang lebih mirip dengan itu IAsyncResult.State, di mana memperkenalkan obat generik tidak akan mudah dilakukan karena tiba-tiba itu akan memperkenalkan obat generik untuk setiap kelas / metode yang terlibat. +1 untuk jawabannya, terutama ... terutama untuk menunjukkan bagian "lebih cepat daripada Java". :]
user541686
1
Oh, dan poin lain: TypedReferencemungkin tidak akan mengalami perubahan yang melanggar dalam waktu dekat, mengingat FieldInfo.SetValueDirect , yang bersifat publik dan mungkin digunakan oleh beberapa pengembang, tergantung padanya. :)
user541686
Ah, tapi Anda jangan perlu metode penyuluhan, dukungan LINQ. Ngomong-ngomong, saya tidak benar-benar berbicara tentang perbedaan yang baik untuk dimiliki / yang perlu dimiliki. Saya tidak akan memanggil TypedReferencesalah satu dari mereka. (Sintaksis yang mengerikan dan ketidaknyamanan keseluruhan mendiskualifikasi, dalam pikiran saya, dari kategori nice-to-have.) Saya akan mengatakan itu hal yang baik untuk dimiliki ketika Anda benar-benar perlu memotong beberapa mikrodetik di sana-sini. Yang mengatakan, saya memikirkan beberapa tempat dalam kode saya sendiri yang akan saya lihat sekarang, untuk melihat apakah saya dapat mengoptimalkannya menggunakan teknik yang Anda tunjukkan.
P Ayah
1
@Merhdad: Saya sedang mengerjakan serializer objek biner / deserializer pada saat itu untuk komunikasi interprocess / interhost (TCP dan pipa). Tujuan saya adalah menjadikannya sekecil mungkin (dalam hal byte yang dikirim melalui kabel) dan secepat mungkin (dalam hal waktu yang dihabiskan untuk membuat serial dan melakukan deserialisasi). Saya pikir saya mungkin menghindari tinju dan unboxing dengan TypedReferences, tetapi IIRC, satu-satunya tempat saya bisa menghindari tinju di suatu tempat adalah dengan elemen array primitif satu dimensi. Manfaat sedikit kecepatan di sini tidak sebanding dengan kompleksitas yang ditambahkan ke seluruh proyek, jadi saya mengambilnya.
P Ayah
1
Diberikan delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);koleksi tipe Tdapat menyediakan metode ActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param), tetapi JITter harus membuat versi metode yang berbeda untuk setiap tipe nilai TParam. Menggunakan referensi yang diketik akan memungkinkan satu versi metode JITted untuk bekerja dengan semua jenis parameter.
supercat
4

Saya tidak tahu apakah judul pertanyaan ini seharusnya sarkastik: Sudah lama diketahui bahwa TypedReferencesepupu yang dikelola dengan benar, bengkak, jelek dari pointer yang dikelola 'benar', yang terakhir adalah apa yang kita dapatkan dengan C ++ / CLI interior_ptr<T> , atau bahkan parameter referensi-tradisional ( ref/ out) dalam C # . Bahkan, cukup sulit untuk TypedReferencemencapai bahkan kinerja dasar hanya dengan menggunakan integer untuk mengindeks ulang dari array CLR asli setiap kali.

Detail menyedihkan ada di sini , tapi untungnya, tidak ada yang penting sekarang ...

Pertanyaan ini sekarang diperdebatkan oleh ref local ref dan fitur pengembalian di C # 7

Fitur-fitur bahasa baru ini memberikan dukungan kelas satu yang menonjol dalam C # untuk mendeklarasikan, berbagi, dan memanipulasi CLR tipe referensi terkelola -tip dalam situasi yang ditentukan dengan cermat.

Pembatasan penggunaan tidak lebih ketat dari apa yang sebelumnya diperlukan untuk TypedReference(dan kinerja benar - benar melompat dari yang terburuk ke yang terbaik ), jadi saya tidak melihat ada use case yang tersisa di C # for TypedReference. Sebagai contoh, sebelumnya tidak ada cara untuk bertahan TypedReferencedi GCheap, jadi hal yang sama berlaku dari pointer yang dikelola atasan sekarang bukan take-away.

Dan jelas, matinya TypedReference— atau paling tidak penghentiannya yang paling lengkap — berarti membuang __makerefsampah itu juga.

Glenn Slayden
sumber