¿Por qué es TypedReference detrás de las escenas? Es tan rápido y seguro ... casi mágico!

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

  •  16-10-2019
  •  | 
  •  

Pregunta

ADVERTENCIA: Esta pregunta es un poco herético ... programadores religiosas siempre cumpliendo con las buenas prácticas, por favor no lo lea. :)

¿Alguien sabe por qué el uso de TypedReference está tan desanimado (implícitamente, por la falta de documentación)?

he encontrado grandes aplicaciones para él, como al pasar parámetros genéricos a través de funciones que no deben ser genérico (cuando se utiliza un object podría ser excesiva o lento, si necesita un tipo de valor), para cuando se necesita una puntero opaco, o para cuando se necesita tener acceso a un elemento de una matriz de forma rápida, cuyas especificaciones se encuentra en tiempo de ejecución (utilizando Array.InternalGetReference). Desde el CLR ni siquiera permite el uso incorrecto de este tipo, ¿por qué es desanimado? No parece que no es seguro ni nada ...


Otros usos que he encontrado para TypedReference:

"especialista" genéricos en C # (esto es de tipo seguro):

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

Escribir código que funciona con punteros genéricos (esto es muy inseguro si se usa mal, pero rápido y seguro si se usa correctamente):

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

Escribir un Método versión de la instrucción sizeof, que puede ser útil en ocasiones:

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

Escribir un método que pasa un parámetro "estado" que quiere evitar el boxeo:

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

¿Por qué son usos como este "desanimados" (por falta de documentación)? Cualquier razones de seguridad, en particular? Parece perfectamente segura y verificable si no se mezcla con punteros (que no son de todos modos seguro o verificable) ...


Actualización:

Código de ejemplo para mostrar que, de hecho, TypedReference puede ser dos veces tan rápido (o más):

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: editar el punto de referencia más arriba, desde la última versión del mensaje utiliza una versión de depuración del código [olvidé de cambiarlo para liberar], y poner ninguna presión sobre la GC Esta versión es un poco más. realista, y en mi sistema, que es más de tres veces más rápido con TypedReference en promedio.)

¿Fue útil?

Solución

Respuesta corta:. portabilidad

Mientras __arglist, __makeref y __refvalue son extensiones de lenguaje y no están documentados en el C # Language Specification, las construcciones utilizadas para ponerlas en práctica bajo el capó (vararg convención de llamada, tipo TypedReference, arglist, refanytype, mkanyref y refanyval instrucciones) están perfectamente documentados en el CLI (ECMA-335 ) en el Vararg biblioteca .

Al estar definida en la Biblioteca Vararg deja bastante claro que están destinados principalmente para apoyar las listas de argumentos de longitud variable y no mucho más. listas variable de argumentos tienen poco uso en plataformas que no necesitan a la interfaz con el código de C externo que utiliza varargs. Por esta razón, la biblioteca Varargs no es parte de cualquier perfil de CLI. implementaciones de CLI legítimas pueden optar por no apoyar la biblioteca Varargs ya que no está incluido en el perfil de la CLI del núcleo:

4.1.6 Vararg

La vararg conjunto de características los soportes listas de argumentos de longitud variable y los punteros de tiempo de ejecución-mecanografiado.

Si se omite: Cualquier intento de hacer referencia a un método con la convención de llamada vararg o las codificaciones de firma asociados a los métodos vararg (véase Partición II) deberá emitir la excepción System.NotImplementedException. Los métodos que utilizan las instrucciones del CIL arglist, refanytype, mkrefany y refanyval deberán emitir la excepción System.NotImplementedException. El momento preciso de la excepción no se especifica. La necesidad System.TypedReference tipo no se puede definir.

Actualizar (Responder al comentario GetValueDirect):

FieldInfo.GetValueDirect son FieldInfo.SetValueDirect están no parte de la biblioteca de clases base. Tenga en cuenta que hay una diferencia entre las clases de .NET Framework Biblioteca y Base Class Library. BCL es el único que se requiere para una aplicación conforme de la CLI / C # y se documenta en ECMA TR / 84 . (De hecho, FieldInfo sí es parte de la biblioteca de reflexión y que no está incluido en el perfil de la CLI del núcleo tampoco).

Una vez que se utiliza un método fuera de BCL, le está dando un poco de la portabilidad (y esto se está convirtiendo cada vez más importante con el advenimiento de non-.NET CLI implementaciones como Silverlight y MonoTouch). Incluso si una implementación quería aumentar Compatiblility con la Biblioteca de Microsoft .NET Framework, podría proporcionar simplemente GetValueDirect y SetValueDirect tomar una TypedReference sin hacer el TypedReference especialmente manipulados por el tiempo de ejecución (básicamente, haciéndolos equivalentes a sus homólogos object sin el beneficio de rendimiento ).

Si hubieran documentado en C #, habría tenido por lo menos un par de implicaciones:

  1. Al igual que cualquier característica, puede convertido en un obstáculo para nuevas características, sobre todo porque éste realmente no encaja en el diseño de C # y requiere extensiones de sintaxis extraños y la entrega especial de un tipo por el tiempo de ejecución.
  2. Todas las implementaciones de C # tienen que implementar alguna manera esta función y no es necesariamente trivial / C # posible que las implementaciones que no se ejecutan en la parte superior de un CLI en absoluto o se ejecutan en la parte superior de un CLI sin Varargs.

Otros consejos

Bueno, yo no soy Eric Lippert, así que no puedo hablar directamente de las motivaciones de Microsoft, pero si tuviera que aventurar una conjetura, diría que TypedReference et al. No están bien documentados porque, francamente, no los necesita.

Cada uso que usted ha mencionado para estas funciones se puede lograr sin ellos, aunque a una reducción del rendimiento en algunos casos. Sin embargo, C # (y .NET en general) no está diseñado para ser un lenguaje de alto rendimiento. (Supongo que "más rápido que Java" era el objetivo de rendimiento.)

Esto no quiere decir que ciertas consideraciones de rendimiento no se ha concedido. De hecho, existen en gran medida las características tales como punteros, stackalloc, y ciertas funciones marco optimizadas para mejorar el rendimiento en ciertas situaciones.

Los genéricos, que yo diría que tiene el rendimiento principal en beneficio de la seguridad de tipos, también mejoran de manera similar a TypedReference evitando el boxeo y unboxing. De hecho, me preguntaba por qué prefiere esto:

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

a esto:

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

Las ventajas y desventajas, como los veo, son que el primero requiere un menor número de equipos conjuntos de investigación (y, se deduce, menos memoria), mientras que el último es más familiar y, asumiría, ligeramente más rápido (evitando desreferencia puntero) .

Me gustaría llamar TypedReference e implementación amigos detalles. Usted ha señalado algunos usos ordenados por ellos, y creo que son vale la pena explorar, pero la advertencia habitual de depender de los detalles de implementación aplica-la próxima versión se puede romper el código.

No se puede averiguar si el título de esta pregunta se supone que es sarcástico: Se ha establecido largo sido que TypedReference es la lenta hinchada primo feo de los 'verdaderos' punteros administrados,,, siendo este último lo que obtenemos con los parámetros C ++ / CLI interior_ptr<T>, o incluso tradicional por referencia (ref / out) en C # . De hecho, es bastante difícil hacer TypedReference incluso llegar a la línea base de rendimiento de usar sólo un entero a re-índice de la matriz original de CLR cada vez.

Los detalles son tristes aquí , pero afortunadamente, nada de esto importa ahora ...

Esta cuestión se consideró dudosa ahora por el nuevo noreferrer locales árbitro y vuelta ref características en C # 7

Estas nuevas características del lenguaje proporcionan prominente apoyo, de primera clase en C # para declarar, el intercambio y la manipulación de cierto CLR gestionado tipo de referencia - tipos de situaciones cuidadosamente prescibed.

Las restricciones de uso son más estrictas que lo que antes se requería para TypedReference (y el rendimiento es literalmente salto de peor a mejor ), así que no veo caso de uso concebible que queda en C # para TypedReference. Por ejemplo, anteriormente no había manera de que persista un TypedReference en el montón GC, por lo que el mismo ser cierto de los punteros administrados superiores ahora no es una comida para llevar.

Y, obviamente, la desaparición de TypedReference-o su casi completa desaprobación en los mínimos medios tirar __makeref en el montón de chatarra también.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top