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


128

Avvertenza: questa domanda è un po 'eretica ... i programmatori religiosi rispettano sempre le buone pratiche, per favore non leggerlo. :)

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

Ho trovato grandi usi per questo, come quando si passano parametri generici attraverso funzioni che non dovrebbero essere generiche (quando si usa un objectpotrebbe essere eccessivo o lento, se è necessario un tipo di valore), per quando è necessario un puntatore opaco, oppure per quando è necessario accedere rapidamente a un elemento di un array, le cui specifiche sono disponibili in fase di esecuzione (utilizzando Array.InternalGetReference). Poiché il CLR non consente nemmeno un utilizzo errato di questo tipo, perché è scoraggiato? Non sembra essere pericoloso o niente ...


Altri usi che ho trovato per TypedReference:

"Specializzazione" generici in C # (questo è sicuro per i tipi):

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

Scrivere codice che funziona 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<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);
}

Scrivere una versione del metodosizeof dell'istruzione, che può essere occasionalmente utile:

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

Scrivere un metodo che passa un parametro "state" che vuole evitare il pugilato:

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

Allora perché gli 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 sicuri o verificabili comunque) ...


Aggiornare:

Codice di esempio per mostrare che, in effetti, TypedReferencepuò essere due volte più veloce (o più):

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

(Modifica: ho modificato il benchmark sopra, poiché l'ultima versione del post ha usato una versione di debug del codice [ho dimenticato di cambiarlo per rilasciarlo] e non ho fatto pressione sul GC. Questa versione è un po 'più realistica e sul mio sistema, è TypedReferencein media più di tre volte più veloce ).


Quando eseguo il tuo esempio ottengo risultati completamente diversi. TypedReference: 203 ticks, boxing/unboxing: 31 ticks. Indipendentemente da ciò che provo (compresi diversi modi per eseguire il cronometraggio) la boxe / unboxing è ancora più veloce sul mio sistema.
Seph

1
@Seph: ho appena visto il tuo commento. È molto interessante: sembra essere più veloce su x64, ma più lento su x86. Strano ...
user541686

1
Ho appena testato quel codice di riferimento sulla mia macchina x64 in .NET 4.5. Ho sostituito Environment.TickCount con Diagnostics.Stopwatch e sono andato con ms invece di tick. Ho eseguito ogni build (x86, 64, Any) tre volte. Il migliore dei tre risultati sono stati i seguenti: x86: 205 / 27ms (stesso risultato per 2/3 corse su questa build) x64: 218 / 109ms Qualsiasi: 205 / 27ms (stesso risultato per 2/3 corse su questa build) In -tutti i casi box / unboxing è stato più veloce.
kornman00,

2
Le strane misurazioni della velocità potrebbero essere attribuite a questi due fatti: * (T) (oggetto) v NON effettua effettivamente un'allocazione dell'heap. In .NET 4+ è ottimizzato via. Non ci sono allocazioni su questo percorso ed è dannatamente veloce. * L'uso di makeref richiede che la variabile sia effettivamente allocata nello stack (mentre il metodo kinda-box potrebbe ottimizzarla in registri). Inoltre, osservando i tempi, presumo che comprometta l'allineamento anche con la bandiera della forza in linea. Quindi kinda-box è integrato e registrato, mentre makeref effettua una chiamata di funzione e gestisce lo stack
hypersw

1
Per vedere i profitti del casting typeref, rendilo meno banale. Ad esempio, lanciare un tipo sottostante nel tipo enum ( int-> DockStyle). Questo box per davvero, ed è quasi dieci volte più lento.
hypersw,

Risposte:


42

Risposta breve: portabilità .

Mentre __arglist, __makerefe __refvaluesono estensioni del linguaggio e sono privi di documenti nel linguaggio C # Specification, i costrutti utilizzati per la loro attuazione sotto il cofano ( varargconvenzione di chiamata, TypedReferencetipo, arglist, refanytype, mkanyref, e refanyvalistruzioni) sono perfettamente documentato nella CLI Specification (ECMA-335) in la biblioteca di Vararg .

Essere definiti nella Libreria Vararg chiarisce che sono principalmente pensati per supportare elenchi di argomenti di lunghezza variabile e non molto altro. Gli elenchi di argomenti variabili hanno scarso utilizzo nelle piattaforme che non necessitano di interfacciarsi con codice C esterno che utilizza varargs. Per questo motivo, la libreria Varargs non fa parte di alcun profilo CLI. Le legittime implementazioni della CLI possono scegliere di non supportare la libreria Varargs poiché non è inclusa nel profilo del kernel della CLI:

4.1.6 Vararg

Il set di funzionalità vararg supporta elenchi di argomenti a lunghezza variabile e puntatori tipizzati in fase di esecuzione.

Se omesso: qualsiasi tentativo di fare riferimento a un metodo con la varargconvenzione di chiamata o le codifiche di firma associate ai metodi vararg (vedere la Partizione II) genererà l' System.NotImplementedExceptioneccezione. I metodi che utilizzano le istruzioni CIL arglist, refanytype, mkrefany, e refanyvaldevono gettare System.NotImplementedExceptionl'eccezione. Il momento esatto dell'eccezione non è specificato. Non è System.TypedReferencenecessario definire il tipo.

Aggiornamento (risposta al GetValueDirectcommento):

FieldInfo.GetValueDirectsono FieldInfo.SetValueDirectsono non parte della Base Class Library. 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 conforme del CLI / C # ed è documentata in ECMA TR / 84 . (In effetti, esso FieldInfostesso fa parte della libreria Reflection e non è nemmeno incluso nel profilo Kernel CLI).

Non appena si utilizza un metodo al di fuori di BCL, si sta abbandonando un po 'di portabilità (e questo sta diventando sempre più importante con l'avvento delle implementazioni CLI non.NET come Silverlight e MonoTouch). Anche se un'implementazione volesse aumentare la compatibilità con la libreria di classi di Microsoft .NET Framework, potrebbe semplicemente fornire GetValueDirecte SetValueDirectprendere a TypedReferencesenza fare il TypedReferenceappositamente gestito dal runtime (sostanzialmente, rendendoli equivalenti alle loro objectcontroparti senza il vantaggio in termini di prestazioni).

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

  1. Come qualsiasi funzione, può diventare un ostacolo per le nuove funzionalità, soprattutto perché questa non si adatta davvero al design di C # e richiede strane estensioni di sintassi e una gestione speciale di un tipo durante il runtime.
  2. Tutte le implementazioni di C # devono implementare in qualche modo questa funzione e non è necessariamente banale / possibile per le implementazioni di C # che non vengono eseguite in cima a una CLI o in esecuzione su una CLI senza Varargs.

4
Buoni argomenti per la portabilità, +1. Ma che dire di FieldInfo.GetValueDirecte FieldInfo.SetValueDirect? Fanno parte del BCL e per usarli è necessario TypedReference , quindi questo non impone sostanzialmente TypedReferencedi essere sempre definito, indipendentemente dalle specifiche del linguaggio? (Inoltre, un'altra nota: anche se le parole chiave non esistessero, purché esistessero le istruzioni, potresti comunque accedervi con metodi di emissione dinamici ... quindi finché la tua piattaforma interagisce con le librerie C, puoi usarle, indipendentemente dal fatto che C # abbia le parole chiave.)
user541686

Oh, e un altro problema: anche se non è portatile, perché non hanno documentato le parole chiave? Per lo meno, è necessario quando si interagisce con C varargs, quindi almeno avrebbero potuto menzionarlo?
user541686,

@Mehrdad: Huh, è interessante. Immagino di aver sempre supposto che i file nella cartella BCL del sorgente .NET facessero parte del BCL, senza mai prestare particolare attenzione alla parte della standardizzazione ECMA. Questo è abbastanza convincente ... tranne una piccola cosa: non è un po 'inutile anche includere la funzione (opzionale) nelle specifiche della CLI, se non c'è documentazione su come usarlo ovunque? (Avrebbe senso se TypedReferencefosse documentato solo per una lingua - diciamo, C ++ gestito - ma se nessuna lingua lo documenta e quindi se nessuno può davvero usarlo, allora perché preoccuparsi di definire la funzione?)
user541686

@Mehrdad Sospetto che la motivazione principale sia stata la necessità di questa funzione internamente per l'interoperabilità ( ad es. [DllImport("...")] void Foo(__arglist); ) E l'hanno implementata in C # per il proprio uso. La progettazione dell'interfaccia della riga di comando è influenzata da molte lingue (le annotazioni "Lo standard annotato per l'infrastruttura linguistica comune" dimostrano questo fatto). Essere un runtime adatto per il maggior numero di lingue possibile, comprese quelle impreviste, è stato sicuramente un obiettivo di progettazione (da cui nome) e questa è una caratteristica di cui, ad esempio, un'ipotetica implementazione C gestita potrebbe trarre vantaggio.
Mehrdad Afshari,

@Mehrdad: Ah ... sì, questa è una ragione abbastanza convincente. Grazie!
user541686

15

Bene, non sono Eric Lippert, quindi non posso parlare direttamente delle motivazioni di Microsoft, ma se dovessi avventurarmi in un'ipotesi, direi che TypedReferenceet al. non sono ben documentati perché, francamente, non ne hai bisogno.

Ogni uso che hai citato per queste funzionalità può essere realizzato senza di esse, anche se in alcuni casi con una penalità di prestazione. Ma C # (e .NET in generale) non è progettato per essere un linguaggio ad alte prestazioni. (Immagino che "più veloce di Java" sia stato l'obiettivo delle prestazioni.)

Questo non vuol dire che alcune considerazioni sulle prestazioni non sono state concesse. In effetti, caratteristiche come i puntatori stackalloce alcune funzioni ottimizzate del framework esistono in gran parte per migliorare le prestazioni in determinate situazioni.

I generici, che direi avere il vantaggio principale della sicurezza dei tipi, migliorano anche le prestazioni in modo simile TypedReferenceevitando boxe e unboxing. In effetti, mi chiedevo perché avresti preferito questo:

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

a questa:

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

I compromessi, come li vedo, sono che il primo richiede meno JIT (e, di conseguenza, meno memoria), mentre il secondo è più familiare e, direi, leggermente più veloce (evitando la dereferenziazione dei puntatori).

Chiamerei i TypedReferencedettagli dell'implementazione degli amici. Hai sottolineato alcuni usi accurati per loro, e penso che valga la pena esplorarli, ma si applica il solito avvertimento di fare affidamento sui dettagli di implementazione: la versione successiva potrebbe violare il codice.


4
Huh ... "non hai bisogno di loro" - Avrei dovuto vederlo arrivare. :-) È vero, ma non è vero. Cosa definisci "bisogno"? I metodi di estensione sono davvero "necessari", ad esempio? Per quanto riguarda la tua domanda sull'uso dei generici in call(): È perché il codice non è sempre così coeso - mi riferivo più a un esempio più simile a quello di IAsyncResult.State, dove introdurre i generici non sarebbe semplicemente fattibile perché all'improvviso introdurrebbe i generici per ogni classe / metodo coinvolto. +1 per la risposta, però ... soprattutto per indicare la parte "più veloce di Java". :]
user541686,

1
Oh, e un altro punto: TypedReferenceprobabilmente non subiremo modifiche in tempi brevi, dato che FieldInfo.SetValueDirect , che è pubblico e probabilmente utilizzato da alcuni sviluppatori, dipende da questo. :)
user541686,

Ah, ma non ha bisogno metodi di estensione, al supporto LINQ. Ad ogni modo, non sto davvero parlando di una differenza piacevole da avere / da avere. Non chiamerei TypedReferencenessuno dei due. (L'atroce sintassi e la disparità complessiva la squalificano, nella mia mente, dalla categoria piacevole da avere.) Direi che è solo una buona cosa avere in giro quando hai davvero bisogno di tagliare qualche microsecondo qua e là. Detto questo, sto pensando a un paio di posti nel mio codice che vado a dare un'occhiata ora, per vedere se posso ottimizzarli usando le tecniche che hai indicato.
P Daddy,

1
@Merhdad: all'epoca stavo lavorando su un serializzatore / deserializzatore di oggetti binari per comunicazioni tra processi / interhost (TCP e pipe). I miei obiettivi erano di renderlo il più piccolo possibile (in termini di byte inviati via cavo) e veloce (in termini di tempo impiegato per serializzare e deserializzare). Pensavo che avrei potuto evitare un po 'di boxe e unboxing con TypedReferences, ma IIRC, l'unico posto in cui sono stato in grado di evitare il pugilato da qualche parte era con gli elementi di array monodimensionali di primitivi. Il leggero vantaggio in termini di velocità qui non valeva la complessità che ha aggiunto all'intero progetto, quindi l'ho eliminato.
P Daddy,

1
Dato delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);un insieme di tipo Tpotrebbe fornire un metodo ActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param), ma il jitter avrebbe dovuto creare una versione diversa del metodo per ogni tipo di valore TParam. L'uso di un riferimento tipizzato consentirebbe a una versione JITted del metodo di funzionare con tutti i tipi di parametri.
Supercat,

4

Non riesco a capire se il titolo di questa domanda dovrebbe essere sarcastico: è stato a lungo stabilito che TypedReferenceè il cugino lento, gonfio, brutto di puntatori gestiti "veri", quest'ultimo è ciò che otteniamo con C ++ / CLI interior_ptr<T> , o anche parametri tradizionali per riferimento ( ref/ out) in C # . In effetti, è abbastanza difficile TypedReferencepersino raggiungere le prestazioni di base usando solo un numero intero per reindicizzare ogni volta l'array CLR originale.

I dettagli tristi sono qui , ma per fortuna, niente di tutto questo conta ora ...

Questa domanda è ora resa discutibile dai nuovi locali di riferimento e dalle funzionalità di restituzione di riferimenti in C # 7

Queste nuove funzionalità linguistiche forniscono un supporto di primo livello di primo piano in C # per la dichiarazione, la condivisione e la manipolazione di CLR tipi di riferimento di tipo gestito reale in situazioni attentamente prescritte.

Le restrizioni sull'uso non sono più rigorose di quanto precedentemente richiesto TypedReference(e le prestazioni stanno letteralmente saltando dal peggiore al migliore ), quindi non vedo alcun caso d'uso rimanente in C # per TypedReference. Ad esempio, in precedenza non c'era modo di persistere TypedReferencea GCnell'heap, quindi lo stesso vale per i puntatori gestiti superiori ora non è un take-away.

E ovviamente, la fine di TypedReference— o almeno la sua deprecazione quasi completa — significa anche buttare __makerefsul mucchio di spazzatura.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.