Perché TypedReference è dietro le quinte? È così veloce e sicuro … quasi magico!

Attenzione: questa domanda è un po ‘eretica … programmatori religiosi che rispettano sempre le buone pratiche, per favore non leggerlo. 🙂

Qualcuno sa perché l’uso di TypedReference è così scoraggiato (implicitamente, per mancanza di documentazione)?

Ho trovato ottimi utilizzi per questo, ad esempio quando si passano parametri generici attraverso funzioni che non dovrebbero essere generiche (quando si usa un object potrebbe essere eccessivo o lento, se è necessario un tipo di valore), per quando è necessario un puntatore opaco, o per quando è necessario accedere rapidamente a un elemento di un array, le cui specifiche si trovano in fase di esecuzione (utilizzando Array.InternalGetReference ). Poiché il CLR non consente nemmeno l’uso errato di questo tipo, perché è scoraggiato? Non sembra essere pericoloso o qualsiasi cosa …


Altri usi che ho trovato per TypedReference :

Generici “specializzati” in C # (questo è sicuro dal tipo):

 static void foo(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); } } 

Scrivere codice che funzioni con puntatori generici (questo è molto pericoloso se usato in modo improprio, ma veloce e sicuro se usato correttamente):

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

Scrivere una versione del metodo dell’istruzione sizeof , che può essere utile occasionalmente:

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

Scrivere un metodo che passa un parametro “stato” che vuole evitare il pugilato:

 static void call(Action 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*/ } } 

Allora perché usi come questo “scoraggiati” (per mancanza di documentazione)? Qualche particolare motivo di sicurezza? Sembra perfettamente sicuro e verificabile se non è mescolato con puntatori (che non sono comunque sicuri o verificabili) …


Aggiornare:

Esempio di codice per mostrare che, in effetti, TypedReference può essere due volte più veloce (o più):

 using System; using System.Collections.Generic; static class Program { static void Set1(T[] a, int i, int v) { __refvalue(__makeref(a[i]), int) = v; } static void Set2(T[] a, int i, int v) { a[i] = (T)(object)v; } static void Main(string[] args) { var root = new List(); 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 } } 

(Modifica: ho modificato il benchmark di cui sopra, poiché l’ultima versione del post ha usato una versione di debug del codice [ho dimenticato di cambiarla per rilasciare], e non ho messo pressione sul GC. Questa versione è un po ‘più realistica, e sul mio sistema, è in media più di tre volte più veloce con TypedReference .)

Risposta breve: portabilità .

Mentre __arglist , __makeref e __refvalue sono estensioni del linguaggio e non sono documentati nella specifica del linguaggio C #, i costrutti utilizzati per implementarli sotto il cappuccio (convenzione di chiamata TypedReference , tipo arglist , refanytype , mkanyref , refanyval e refanyval ) sono perfettamente documentati in la specifica CLI (ECMA-335) nella libreria Vararg .

Essere definiti nella libreria Vararg rende abbastanza chiaro che essi sono principalmente pensati per supportare liste di argomenti di lunghezza variabile e non molto altro. Gli elenchi di argomenti variabili hanno poco utilizzo su piattaforms che non hanno bisogno di interfacciarsi con il codice C esterno che utilizza vararg. Per questo motivo, la libreria Varargs non fa parte di alcun profilo CLI. Le implementazioni CLI legittime possono scegliere di non supportare la libreria Varargs in quanto non è inclusa nel profilo del kernel CLI:

4.1.6 Vararg

Il set di funzioni vararg supporta elenchi di argomenti di lunghezza variabile e puntatori runtime.

Se omesso: Qualsiasi tentativo di fare riferimento a un metodo con la convenzione di chiamata vararg o le codifiche di firma associate ai metodi vararg (vedere Partizione II) genererà l’eccezione System.NotImplementedException . I metodi che usano le istruzioni CIL, refanytype , mkrefany e refanyval generano l’eccezione System.NotImplementedException . Il momento preciso dell’eccezione non è specificato. Non è necessario definire il tipo System.TypedReference .

Aggiornamento (rispondere al commento GetValueDirect ):

FieldInfo.GetValueDirect sono FieldInfo.SetValueDirect non fanno parte della libreria di classi base. Si noti che esiste una differenza tra la libreria di classi .NET Framework e la libreria di classi base. BCL è l’unica cosa necessaria per un’implementazione conforms di CLI / C # ed è documentata in ECMA TR / 84 . (Infatti, FieldInfo stesso fa parte della libreria Reflection e non è incluso nel profilo del kernel CLI).

Non appena si utilizza un metodo esterno a BCL, si rinuncia a un po ‘di portabilità (e questo sta diventando sempre più importante con l’avvento di implementazioni CLI non-. NET come Silverlight e MonoTouch). Anche se un’implementazione voleva aumentare la compatibilità con la libreria di classi Microsoft .NET Framework, poteva semplicemente fornire GetValueDirect e SetValueDirect prendendo una TypedReference senza rendere il TypedReference gestito appositamente dal runtime (in pratica, rendendoli equivalenti alle loro controparti object senza il vantaggio delle prestazioni ).

Se lo avessero documentato in C #, avrebbe avuto almeno un paio di implicazioni:

  1. Come qualsiasi altra funzionalità, potrebbe diventare un ostacolo alle nuove funzionalità, soprattutto perché questo non si adatta perfettamente alla progettazione di C # e richiede estensioni di syntax strane e una consegna speciale di un tipo da parte del runtime.
  2. Tutte le implementazioni di C # devono in qualche modo implementare questa funzionalità e non è necessariamente banale / ansible per implementazioni C # che non vengono eseguite su una CLI o che vengono eseguite su CLI senza Varargs.

Beh, io non sono Eric Lippert, quindi non posso parlare direttamente delle motivazioni di Microsoft, ma se dovessi azzardare un’ipotesi, direi che TypedReference et al. non sono ben documentati perché, francamente, non ne hai bisogno.

Ogni uso che hai citato per queste caratteristiche può essere realizzato senza di loro, anche se in alcuni casi può comportare una penalizzazione delle prestazioni. Ma C # (e .NET in generale) non è progettato per essere un linguaggio ad alte prestazioni. (Sto indovinando che “più veloce di Java” era l’objective delle prestazioni.)

Questo non vuol dire che alcune considerazioni sulle prestazioni non siano state fornite. In effetti, funzioni come puntatori, stackalloc e alcune funzioni di framework ottimizzate esistono in gran parte per migliorare le prestazioni in determinate situazioni.

I generici, che direi che hanno il vantaggio principale della sicurezza del tipo, migliorano anche le prestazioni in modo simile a TypedReference evitando il pugilato e l’unboxing. In effetti, mi stavo chiedendo perché preferiresti questo:

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

a questa:

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

I trade-off, come li vedo, sono che il primo richiede meno JIT (e ne consegue, meno memoria), mentre il secondo è più familiare e, presumo, leggermente più veloce (evitando il dereferenziamento del puntatore).

TypedReference dettagli di implementazione di TypedReference e degli amici. Hai indicato alcuni usi accurati per loro, e penso che valga la pena esplorare, ma vale la solita osservazione dei dettagli di implementazione: la prossima versione potrebbe infrangere il tuo codice.

Non riesco a capire se il titolo di questa domanda debba essere sarcastico: è stato stabilito da molto tempo che TypedReference è il lento, gonfio, brutto cugino di puntatori gestiti “veri”, vale a dire, ciò che otteniamo con C ++ / CLI interior_ptr , o anche i tradizionali riferimenti di riferimento ( ref / out ) in C # . In effetti, è abbastanza difficile rendere TypedReference persino in grado di raggiungere le prestazioni di base semplicemente utilizzando un numero intero per TypedReference l’array CLR originale ogni volta.

I dettagli tristi sono qui , ma per fortuna nessuno di questi aspetti ora …

Questa domanda è ora resa discutibile dai nuovi ref-locals e dalle funzionalità di return ref in C # 7

Queste nuove funzionalità del linguaggio forniscono un supporto di primo livello in C # per dichiarare, condividere e manipolare tipi di tipi di riferimento gestiti CLR reali in situazioni accuratamente prescritte.

Le restrizioni sull’utilizzo non sono più severe di quelle richieste in precedenza per TypedReference (e le prestazioni stanno letteralmente saltando dal peggiore al migliore ), quindi non vedo nessun caso d’uso concepibile in C # per TypedReference . Ad esempio, in precedenza non c’era modo di mantenere un valore TypedReference del GC , quindi lo stesso valore dei puntatori gestiti superiori ora non è un take-away.

E ovviamente, la scomparsa di TypedReference – o TypedReference sua quasi totale deprecazione – significa anche buttare __makeref sul junkheap.