TypedReference neden perde arkasında? Çok hızlı ve güvenli… neredeyse büyülü!


128

Uyarı: Bu soru biraz sapkın ... Dindar programcılar her zaman iyi uygulamalara bağlı kalıyorlar, lütfen okumayın. :)

TypedReference kullanımının neden bu kadar cesaret kırıldığını bilen var mı (örtük olarak, belge eksikliği nedeniyle)?

objectOpak bir işaretçiye ihtiyaç duyduğunuzda, genel parametreleri genel olmaması gereken işlevlerden geçirirken ( bir değer türüne ihtiyacınız varsa aşırı veya yavaş olabilir) , bunun için harika kullanım alanları buldum veya bir dizinin bir öğesine hızlı bir şekilde erişmeniz gerektiğinde, özelliklerini çalışma zamanında bulduğunuzda (kullanarak Array.InternalGetReference). CLR bu türün yanlış kullanımına bile izin vermediğine göre, neden önerilmiyor? Güvensiz falan görünmüyor ...


Bulduğum diğer kullanımlar TypedReference:

C # 'da "özelleştirme" jenerikleri (bu tür açısından güvenlidir):

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

Genel işaretçilerle çalışan kod yazma (bu, yanlış kullanıldığında çok güvensiz, ancak doğru kullanıldığında hızlı ve güvenlidir):

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

Bazen yararlı olabilecek talimatın bir yöntem versiyonunun yazılması sizeof:

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

Kutudan kaçınmak isteyen bir "durum" parametresini geçiren bir yöntem yazma:

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

Öyleyse neden bu tür kullanımlar "önerilmiyor" (belge eksikliği nedeniyle)? Herhangi bir özel güvenlik nedeni var mı? İşaretçilerle karıştırılmadıysa (zaten güvenli veya doğrulanamaz) tamamen güvenli ve doğrulanabilir görünüyor ...


Güncelleme:

Gerçekten TypedReferenceiki kat daha hızlı (veya daha fazla) olabileceğini gösteren örnek kod :

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

(Düzenleme: Gönderinin son sürümü kodun hata ayıklama sürümünü kullandığından [yayınlamak için değiştirmeyi unuttum] ve GC üzerinde hiçbir baskı oluşturmadığından yukarıdaki karşılaştırmayı düzenledim. Bu sürüm biraz daha gerçekçi ve sistemimde TypedReferenceortalamada üç kattan daha hızlı .)


Örneğinizi çalıştırdığımda tamamen farklı sonuçlar alıyorum. TypedReference: 203 ticks, boxing/unboxing: 31 ticks. Ne denersem deneyeyim (zamanlamayı yapmanın farklı yolları dahil) sistemimde boks / kutudan çıkarma işlemi hala daha hızlı.
Seph

1
@Seph: Yorumunuzu az önce gördüm. Bu çok ilginç - x64'te daha hızlı, ancak x86'da daha yavaş görünüyor. Weird ...
user541686

1
Bu karşılaştırma kodunu x64 makinemde .NET 4.5 altında test ettim. Environment.TickCount'u Diagnostics.Stopwatch ile değiştirdim ve ticks yerine ms ile gittim. Her yapıyı (x86, 64, Herhangi biri) üç kez çalıştırdım. Üç sonuçtan en iyisi follws: x86: 205 / 27ms (bu yapıda 2/3 çalıştırma için aynı sonuç) x64: 218 / 109ms Herhangi: 205 / 27ms (bu derlemede 2/3 çalıştırma için aynı sonuç) -all case box / unboxing daha hızlıydı.
kornman00

2
Garip hız ölçümleri şu iki olguyla ilişkilendirilebilir: * (T) (nesne) v aslında bir yığın tahsisi YAPMAZ. .NET 4+ içinde optimize edilmiştir. Bu yolda tahsis yok ve çok hızlı. * Makeref'i kullanmak, değişkenin yığında gerçekten tahsis edilmesini gerektirir (kinda-box yöntemi onu kayıtlar halinde optimize edebilir). Ayrıca, zamanlamalara bakarak, zorla satır içi bayrağıyla bile satır içi yapmayı bozduğunu varsayıyorum. Yani kinda-box satır içi ve kayıtlıdır, oysa makeref bir işlev çağrısı yapar ve yığını çalıştırır
hypersw

1
Typeref dökümünün karını görmek için daha az önemsiz hale getirin. Örneğin, enum türüne ( int-> DockStyle) temel bir tür yayınlama . Bu kutular gerçektir ve neredeyse on kat daha yavaştır.
hypersw

Yanıtlar:


42

Kısa cevap: taşınabilirlik .

İken __arglist, __makerefve __refvaluevardır dil uzantıları ve C # Dili Şartnamede belgesiz vardır, kullanılan yapılar (başlık altında bunları uygulamak için varargçağırma,TypedReference tip, arglist, refanytype, mkanyref, ve refanyvaltalimatlar) mükemmel belgelenir CLI Şartname (ECMA-335) içinde vararg kütüphanesi .

Vararg Kitaplığı'nda tanımlanmış olmaları, bunların öncelikle değişken uzunluklu argüman listelerini desteklemeyi amaçladıklarını ve başka pek bir şey olmadığını açıkça ortaya koymaktadır. Değişken bağımsız değişken listelerinin, varargs kullanan harici C koduyla arayüz oluşturması gerekmeyen platformlarda çok az kullanımı vardır. Bu nedenle, Varargs kitaplığı herhangi bir CLI profilinin parçası değildir. Meşru CLI uygulamaları, CLI Kernel profilinde yer almadığı için Varargs kitaplığını desteklememeyi seçebilir:

4.1.6 Vararg

Vararg özellik seti sayısı değişken listeleri ve çalışma zamanı-daktilo işaretçileri destekler.

Atlanırsa: ile bir yöntem referans için herhangi bir girişim varargçağrı kuralı veya atmak eder vararg yöntemleri (Partition II) ile bağlantılı imza kodlama System.NotImplementedExceptiondurum. CIL yönergeleri kullanarak Yöntemleri arglist, refanytype, mkrefanyverefanyval atmak zorundadır System.NotImplementedExceptionistisna. İstisnanın kesin zamanlaması belirtilmemiştir. Tipi System.TypedReferencetanımlanmış gerek yoktur.

Güncelleme ( GetValueDirectyoruma cevap verin ):

FieldInfo.GetValueDirectHangi FieldInfo.SetValueDirectvardır değil Temel Sınıf Kütüphanesi parçası. .NET Framework Sınıf Kitaplığı ile Temel Sınıf Kitaplığı arasında bir fark olduğunu unutmayın. BCL, uyumlu bir CLI / C # uygulaması için gereken tek şeydir ve ECMA TR / 84'te belgelenmiştir . (Aslında FieldInfokendisi Yansıma kütüphanesinin bir parçasıdır ve CLI Kernel profiline de dahil değildir).

BCL dışında bir yöntemi kullanır kullanmaz, biraz taşınabilirlikten vazgeçmiş oluyorsunuz (ve bu, Silverlight ve MonoTouch gibi .NET dışı CLI uygulamalarının ortaya çıkmasıyla giderek daha önemli hale geliyor). Bir uygulama Microsoft .NET Framework Sınıf Kitaplığı ile compatiblility artırmak istesem bile, bu sadece sağlayabilir GetValueDirectve SetValueDirectbir alma TypedReferenceyapmadanTypedReference (onlara eşdeğer hale temelde özel olarak çalışma zamanı tarafından ele objectperformans yararı olmadan muadilleri).

C # ile belgelemiş olsalardı, en azından birkaç sonucu olurdu:

  1. Herhangi bir özellik gibi , bu gerçekten C # tasarımında sığacak ve garip sözdizimi uzantıları ve çalışma zamanı tarafından bir tür özel teslimini gerektiren gelmez, özellikle de yeni özelliklere engelliyor.
  2. Tüm C # uygulamaları bir şekilde bu özelliği uygulamak zorundadır ve bu, bir CLI'nın üstünde hiç çalışmayan veya Varargs'sız bir CLI'nın üstünde çalışan C # uygulamaları için önemsiz / mümkün değildir.

4
Taşınabilirlik için iyi argümanlar, +1. Peki ya FieldInfo.GetValueDirectve FieldInfo.SetValueDirect? BCL'nin bir parçasıdırlar ve ihtiyacınız TypedReference olan onları kullanmak için , bu temelde TypedReferencedil spesifikasyonundan bağımsız olarak her zaman tanımlanmaya zorlamaz mı? (Ayrıca, başka bir not: Anahtar kelimeler olmasa bile, talimatlar var olduğu sürece, onlara dinamik olarak yayan yöntemlerle erişebilirsiniz ... platformunuz C kütüphaneleri ile birlikte çalıştığı sürece, bunları kullanabilirsiniz, C # anahtar kelimelere sahip olsun ya da olmasın.)
user541686

Oh, ve başka bir konu: Taşınabilir olmasa bile, anahtar kelimeleri neden belgelemediler? En azından, C vararg'ları ile etkileşim halindeyken gerekli, yani en azından bundan bahsedebilirler miydi?
user541686

@Mehrdad: Huh, bu ilginç. Sanırım her zaman .NET kaynağının BCL klasöründeki dosyaların BCL'nin bir parçası olduğunu varsaydım , ECMA standardizasyon kısmına asla gerçekten dikkat etmedim. Bu oldukça ikna edici ... küçük bir şey dışında: (isteğe bağlı) özelliği, herhangi bir yerde nasıl kullanılacağına dair bir belge yoksa, CLI spesifikasyonuna dahil etmek bile biraz anlamsız değil mi? (Yalnızca TypedReferencebir dil için belgelenmiş olsaydı mantıklı olurdu - örneğin, C ++ yönetildi - ama eğer hiçbir dil onu belgelemiyorsa ve bu yüzden kimse onu gerçekten kullanamıyorsa, o zaman neden özelliği tanımlama zahmetine
girsin ki

@Mehrdad Birincil motivasyonun interop için dahili olarak bu özelliğe ihtiyaç duyduğundan şüpheleniyorum ( örneğin [DllImport("...")] void Foo(__arglist); ) ve kendi kullanımları için C # ile uyguladılar. CLI'nın tasarımı pek çok dilden etkilenmiştir ("Ortak Dil Altyapısı Açıklamalı Standart" notları bu gerçeği göstermektedir.) Öngörülemeyenler de dahil olmak üzere olabildiğince çok dil için uygun bir çalışma süresi olmak kesinlikle bir tasarım hedefi olmuştur (dolayısıyla name) ve bu, örneğin varsayımsal olarak yönetilen bir C uygulamasının muhtemelen yararlanabileceği bir özelliktir.
Mehrdad Afshari

@Mehrdad: Ah ... evet, bu oldukça ikna edici bir sebep. Teşekkürler!
user541686

15

Ben Eric Lippert değilim, bu yüzden doğrudan Microsoft'un motivasyonlarından söz edemem, ancak bir tahminde bulunacak olsaydım, şunu söylerdim TypedReference ve diğerleri . iyi belgelenmedi çünkü açıkçası onlara ihtiyacınız yok.

Bu özellikler için bahsettiğiniz her kullanım, bazı durumlarda performans cezası olsa da bunlar olmadan gerçekleştirilebilir. Ancak C # (ve genel olarak .NET) yüksek performanslı bir dil olarak tasarlanmamıştır. (Performans hedefinin "Java'dan daha hızlı" olduğunu tahmin ediyorum.)

Bu, belirli performans değerlendirmelerinin karşılanmadığı anlamına gelmez. Nitekim, işaretçiler gibi özellikler,stackalloc ve belirli optimize edilmiş çerçeve işlevleri, büyük ölçüde belirli durumlarda performansı artırmak için mevcuttur.

Tip güvenliğinin birincil faydasına sahip olduğunu söyleyeceğim jenerikler, aynı zamanda TypedReferencekutudan ve kutudan çıkarmadan kaçınarak performansı artırıyor. Aslında, bunu neden tercih ettiğini merak ediyordum:

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

buna:

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

Gördüğüm gibi, değiş tokuşlar, birincisinin daha az JIT gerektirmesi (ve bunu takiben daha az bellek), ikincisinin daha tanıdık ve biraz daha hızlı olduğunu varsayıyorum (işaretçi referansından kaçınarak).

TypedReferenceUygulama detayları arar ve arkadaşlarım. Onlar için bazı düzgün kullanımlara işaret ettiniz ve bence keşfetmeye değer, ancak uygulama ayrıntılarına güvenmenin olağan uyarısı geçerlidir - bir sonraki sürüm kodunuzu kırabilir.


4
Huh ... "onlara ihtiyacın yok" - Bunun olacağını görmeliydim. :-) Bu doğru ama aynı zamanda doğru değil. Neyi "ihtiyaç" olarak tanımlıyorsunuz? Örneğin, uzatma yöntemleri gerçekten "gerekli" midir? Jeneriklerin kullanımıyla ilgili sorunuza gelince call(): Bunun nedeni, kodun her zaman çok uyumlu olmamasıdır - daha çok, IAsyncResult.Statejeneriklerin tanıtılmasının mümkün olmadığı, çünkü birdenbire jeneriklerin dahil olan her sınıf / yöntem. Cevap için +1 olsa da ... özellikle "Java'dan daha hızlı" kısmına işaret ettiği için. :]
user541686

1
Oh, ve başka bir nokta: Halka açık olan ve muhtemelen bazı geliştiriciler tarafından kullanılan FieldInfo.SetValueDirect'in buna bağlı olduğu göz önüne alındığında TypedReference, muhtemelen yakın zamanda değişikliklere uğramayacak . :)
user541686

Ah, ama do destek LINQ için, uzatma yöntemleri gerekir. Her neyse, gerçekten sahip olunması / olması gereken bir farktan bahsetmiyorum. Ben TypedReferenceikisini de aramazdım. (Acımasız sözdizimi ve genel beceriksizlik, zihnimde onu sahip olunması iyi olan kategorisinden diskalifiye eder.) Burada ve orada birkaç mikrosaniyeyi gerçekten kırpmanız gerektiğinde, etrafta olması iyi bir şey diyebilirim. Bununla birlikte, kendi kodumda, belirttiğiniz teknikleri kullanarak onları optimize edip edemeyeceğimi görmek için hemen bakacağım birkaç yer düşünüyorum.
P Baba

1
@Merhdad: Süreçler arası / ana bilgisayarlar arası iletişim (TCP ve borular) için bir ikili nesne serileştirici / seri çözücü üzerinde çalışıyordum. Hedeflerim, onu olabildiğince küçük (kablo üzerinden gönderilen bayt cinsinden) ve hızlı (serileştirme ve seriyi kaldırma için harcanan zaman açısından) yapmaktı. TypedReferenceS ile bazı kutudan ve kutudan çıkarmadan kaçınabileceğimi düşündüm , ancak IIRC, bir yerde boks yapmaktan kaçınabildiğim tek yer , ilkellerin tek boyutlu dizilerinin öğeleriydi. Buradaki hafif hız avantajı, tüm projeye eklediği karmaşıklığa değmedi, bu yüzden onu çıkardım.
P Daddy

1
delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);Bir tür koleksiyonu verildiğinde T, bir yöntem sağlayabilir ActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param), ancak JITter'in her değer türü için yöntemin farklı bir sürümünü oluşturması gerekir TParam. Yazılı bir referans kullanmak, yöntemin bir JIT uygulanmış sürümünün tüm parametre türleriyle çalışmasına izin verir.
supercat

4

Bu sorunun başlığının alaycı olup olmadığını anlayamıyorum: Uzun zamandır bilinen bu TypedReference, 'gerçek' yönetilen işaretçilerin yavaş, şişkin, çirkin kuzeni, ikincisi C ++ / CLI ile elde ettiğimiz şey interior_ptr<T>veya C # 'ta geleneksel referansla ( ref/ out) parametreler bile . Aslında, her seferinde orijinal CLR dizisini yeniden indekslemek için yalnızca bir tamsayı kullanmanın temel performansına ulaşmak bile oldukça zordur .TypedReference

Üzücü detaylar burada , ama şükürler olsun ki artık bunların hiçbiri önemli değil ...

Bu soru artık yeni ref yerelleri ve C # 7'deki ref dönüş özellikleri tarafından tartışmalı hale getirildi.

Bu yeni dil özellikleri , dikkatli bir şekilde önceden belirlenmiş durumlarda gerçek yönetilen referans türü türlerini bildirmek, paylaşmak ve işlemek için C #'da önemli, birinci sınıf destek sağlar .CLR

Kullanım kısıtlamaları daha önce gerekli olandan daha katı değildir TypedReference(ve performans kelimenin tam anlamıyla en kötüden en iyiye atlamaktadır ), bu nedenle C # for TypedReference. Örneğin, daha önce bir yığın TypedReferenceiçinde kalıcı GColmanın bir yolu yoktu , bu nedenle aynı şey artık üstün yönetilen işaretçiler için de geçerli değil.

Ve açıkçası, TypedReferenceen azından neredeyse tamamen kullanımdan kaldırılmasının sona ermesi, önemsiz yere de atmak anlamına gelir __makeref.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.