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

StackOverflow https://stackoverflow.com/questions/4764573

  •  16-10-2019
  •  | 
  •  

Domanda

Attenzione: Questa domanda è un po 'eretica ... programmatori religiose rispettosi sempre da buone pratiche, si prega di non leggerlo. :)

Qualcuno sa il motivo per cui l'uso di TypedReference è così scoraggiato (implicitamente, dalla mancanza di documentazione)?

ho trovato grandi usi per esso, come ad esempio quando il passaggio di parametri generici attraverso le funzioni che non dovrebbero essere generico (quando si utilizza un object potrebbe essere eccessivo o lento, se avete bisogno di un tipo di valore), per quando avete bisogno di un puntatore opaco, o per quando si ha bisogno di accedere ad un elemento di un array in modo rapido, le cui specifiche si trova in fase di esecuzione (utilizzando Array.InternalGetReference). Dal momento che il CLR non permette nemmeno uso non corretto di questo tipo, perché è scoraggiato? Non sembra essere pericoloso o niente ...


Altri usi che ho trovato per TypedReference:

"Specializzato" generici in C # (questo è type-safe):

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

La scrittura di codice che funziona con i puntatori generici (questo è molto non sicuro se abusato, 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);
}

Scrivi metodo Versione dell'istruzione sizeof, che può essere a volte 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 "stato" che vuole evitare la boxe:

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é sono usi come questo "scoraggiati" (per mancanza di documentazione)? Eventuali particolari motivi di sicurezza? Sembra perfettamente sicuro e verificabile se non è mescolato con puntatori (che non sono comunque sicuro o verificabile) ...


Aggiornamento:

Il codice di esempio per dimostrare 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>(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
    }
}

(Edit: ho modificato il punto di riferimento di cui sopra, dal momento che l'ultima versione del post utilizzata una versione di debug del codice [ho dimenticato di cambiarlo a rilasciare], e mettere alcuna pressione sul GC Questa versione è un po 'di più. realistica, e sul mio sistema, è più di tre volte più veloce con TypedReference in media).

È stato utile?

Soluzione

Risposta breve:. portabilità

Mentre __arglist, __makeref e __refvalue sono estensioni del linguaggio e sono privi di documenti nel linguaggio C # Specification, i costrutti utilizzati per la loro attuazione sotto il cofano (vararg convenzione di chiamata, tipo TypedReference, arglist, refanytype, mkanyref, e refanyval istruzioni) sono perfettamente documentate nel CLI Specification (ECMA-335 ) nel biblioteca vararg .

Essere definito nella Biblioteca vararg rende abbastanza chiaro che essi sono destinati in primo luogo a sostenere liste di argomenti a lunghezza variabile e non molto altro. liste variabile di argomenti hanno poco utilizzo in piattaforme che non hanno bisogno di interfacciarsi con il codice C esterno che utilizza varargs. Per questo motivo, la libreria varargs non è parte di qualsiasi profilo CLI. implementazioni CLI legittimi possono scegliere di non supportare libreria varargs come non è incluso nel profilo CLI Kernel:

4.1.6 vararg

Il vararg set di funzionalità supporti a lunghezza variabile liste di argomenti e puntatori runtime-digitato.

Se omesso: Qualsiasi tentativo di fare riferimento a un metodo con la convenzione di chiamata vararg o le codifiche firma associati ai metodi vararg (vedi Partizione II) deve generare l'eccezione System.NotImplementedException. I metodi che utilizzano l'istruzioni CIL arglist, refanytype, mkrefany, e refanyval devono generare l'eccezione System.NotImplementedException. Il tempo esatto della eccezione non è specificato. La necessità di tipo System.TypedReference non essere definito.

Update (Rispondi al commento GetValueDirect):

FieldInfo.GetValueDirect sono FieldInfo.SetValueDirect sono non parte della Base Class Library. Si noti che c'è una differenza tra il .NET Framework e Libreria di classi Base. BCL è l'unica cosa necessaria per un conforme attuazione della CLI / C # ed è documentata in ECMA TR / 84 . (In realtà, FieldInfo stessa è parte della libreria di riflessione e che non è incluso nel profilo CLI Kernel o).

Non appena si utilizza un metodo di fuori BCL, si stanno dando un po 'di portabilità (e questo sta diventando sempre più importante con l'avvento di non-.NET CLI implementazioni come Silverlight e MonoTouch). Anche se un'implementazione voluto aumentare Compatiblility con la libreria di classi di Microsoft .NET Framework, potrebbe semplicemente fornire GetValueDirect e SetValueDirect prendendo un TypedReference senza fare la TypedReference appositamente gestito dal runtime (in pratica, rendendoli equivalenti alle loro controparti object senza il beneficio delle prestazioni ).

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

  1. Come ogni funzione, possono diventa un ostacolo alle nuove caratteristiche, soprattutto perché questo non si adatta veramente nella progettazione di C # e richiede le estensioni di sintassi strano e speciale consegna di un tipo da parte del runtime.
  2. Tutte le implementazioni di C # devono implementare in qualche modo questa funzione e non è necessariamente banale / possibile per C # implementazioni che non vengono eseguiti sulla cima di una CLI affatto o vengono eseguiti sulla cima di una CLI, senza varargs.

Altri suggerimenti

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 documentate perché, francamente, non ne hanno bisogno.

Ogni uso che lei ha citato per queste caratteristiche può essere realizzato senza di loro, anche se a una riduzione delle prestazioni in alcuni casi. Ma C # (e .NET in generale) non è progettato per essere un linguaggio ad alto rendimento. (Sto indovinando che "più veloce di Java" era l'obiettivo delle prestazioni.)

Questo non vuol dire che certe considerazioni sulle prestazioni non sono stati offerto. In effetti, esistono caratteristiche come puntatori, stackalloc, e alcune funzioni ottimizzate quadro in gran parte a migliorare le prestazioni in determinate situazioni.

Generics, che direi avere le prestazioni principale vantaggio della sicurezza tipo, migliorare anche in modo simile a TypedReference evitando boxe e unboxing. In effetti, mi chiedevo perché si preferisce in questo modo:

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

a questo:

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

I compromessi, come li vedo, sono che il primo richiede un minor numero di squadre investigative comuni (e, ne consegue, meno memoria), mentre il secondo è più familiare e, vorrei assumere, leggermente più veloce (evitando puntatore dereferenziazione) .

mi piacerebbe chiamare TypedReference e implementazione amici particolari. Hai fatto notare alcuni usi pulito per loro, e penso che siano vale la pena esplorare, ma la solita avvertenza di basarsi su dettagli di implementazione applica-la prossima versione potrebbe rompere il vostro codice.

Non riesco a capire se il titolo di questa domanda dovrebbe essere sarcastico: E 'stato di lunga data che TypedReference è il gonfio cugino brutto lenta, di 'veri' puntatori gestiti, quest'ultimo è quello che si ottiene con i parametri C ++ / CLI interior_ptr<T>, o anche tradizionale per riferimento (ref / out) in C # . In realtà, è piuttosto difficile fare TypedReference anche raggiungere il prestazioni di base di solo utilizzando un numero intero di ri-index off dell'array CLR originale ogni volta.

I dettagli sono tristi qui , ma per fortuna, niente di tutto questo importa adesso ...

Questa domanda è ora vanificate dal nuovo noreferrer locali ref e ritorno ref le caratteristiche in C # 7

Queste nuove funzionalità del linguaggio forniscono prominente, il supporto di prima classe a C # per la dichiarazione, la condivisione e la manipolazione vera CLR tipo di riferimento gestita - tipi in situazioni attentamente prescibed.

Le restrizioni d'uso non sono più rigorosi di quanto in precedenza richiesto per TypedReference (e la performance è letteralmente salto dal peggiore al migliore ), quindi non vedo restante caso d'uso concepibile in C # per TypedReference. Ad esempio, in precedenza non c'era modo a persistere un TypedReference nel mucchio GC, quindi lo stesso essere vero dei puntatori gestite superiori ora non è un take-away.

E ovviamente, la scomparsa di TypedReference-o la sua quasi completa disapprovazione a dei minimi mezzi gettare __makeref sul rottame pure.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top