c# typedreference

c# - ¿Por qué TypedReference está detrás de escena? Es tan rápido y seguro... ¡casi mágico!



(3)

Advertencia: Esta pregunta es un poco herética ... los programadores religiosos siempre acatan las buenas prácticas, por favor no lo lean. :)

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

Encontré un gran uso para ello, como cuando paso parámetros genéricos a través de funciones que no deberían ser genéricas (cuando se usa un object puede ser excesivo o lento, si necesita un tipo de valor), cuando necesita un puntero opaco, o para cuando necesite acceder rápidamente a un elemento de una matriz, cuyas especificaciones encuentre en tiempo de ejecución (usando Array.InternalGetReference ). Dado que el CLR ni siquiera permite el uso incorrecto de este tipo, ¿por qué se desaconseja? No parece ser inseguro ni nada ...

Otros usos que he encontrado para TypedReference :

Genéricos "especializados" en C # (esto es 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 funcione con punteros genéricos (esto es muy inseguro si se utiliza incorrectamente, pero es 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 una versión de método del sizeof instrucción, que a veces puede ser útil:

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 pase un parámetro de "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*/ } }

Entonces, ¿por qué los usos como este "se desalientan" (por falta de documentación)? ¿Alguna razón de seguridad en particular? Parece perfectamente seguro y verificable si no está mezclado con punteros (que de todos modos no son seguros ni verificables) ...

Actualizar:

Código de muestra para mostrar que, de hecho, TypedReference puede ser el doble de 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 } }

(Editar: edité el punto de referencia anterior, ya que la última versión de la publicación utilizaba una versión de depuración del código [Olvidé cambiarla a versión], y no pongo presión sobre el GC. Esta versión es un poco más realista, y en mi sistema, es más de tres veces más rápido con TypedReference en promedio).


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

Todos los usos que mencionó para estas características se pueden lograr sin ellos, aunque con una penalización de rendimiento en algunos casos. Pero C # (y .NET en general) no está diseñado para ser un lenguaje de alto rendimiento. (Supongo que "más rápido que Java" fue el objetivo de rendimiento).

Eso no quiere decir que ciertas consideraciones de rendimiento no se hayan permitido. De hecho, características tales como punteros, stackalloc y ciertas funciones de marco optimizadas existen en gran medida para aumentar el rendimiento en ciertas situaciones.

Los genéricos, que diría que tienen el beneficio principal de la seguridad del tipo, también mejoran el rendimiento de manera similar a TypedReference al evitar el boxeo y el desempaquetado. De hecho, me preguntaba por qué preferirías 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 concesiones, según las veo, son que la primera requiere menos JIT (y, como se sigue, menos memoria), mientras que la segunda es más familiar y, supongo, un poco más rápida (al evitar la desreferenciación del puntero).

TypedReference a TypedReference y a los detalles de implementación de amigos. Ha señalado algunos usos útiles para ellos, y creo que vale la pena explorarlos, pero se aplica la advertencia habitual de confiar en los detalles de implementación: la próxima versión puede romper su código.


No puedo entender si se supone que el título de esta pregunta es sarcástico: desde hace long-established que TypedReference es el primo lento, hinchado y feo de punteros administrados "verdaderos", es decir, lo que obtenemos con C ++ / CLI interior_ptr<T> , o incluso parámetros tradicionales de referencia ( ref / out ) en C # . De hecho, es bastante difícil hacer que TypedReference alcance el rendimiento de línea de base de solo usar un entero para volver a indexar la matriz CLR original todo el tiempo.

Los tristes detalles están long-established , pero afortunadamente, nada de esto importa ahora ...

Esta pregunta ahora es irrelevante por los nuevos locales de referencia y las funciones de devolución de ref en C # 7

Estas nuevas funciones de lenguaje proporcionan un destacado soporte de primera clase en C # para declarar, compartir y manipular tipos de tipo de referencia gestionados por CLR verdaderos en situaciones cuidadosamente planteadas.

Las restricciones de uso no son más estrictas que las requeridas previamente para TypedReference (y el rendimiento está literalmente long-established ), así que no veo caso de uso concebible en C # para TypedReference . Por ejemplo, anteriormente no había forma de que persistiera una TypedReference en el montón de GC , por lo que lo mismo ocurre con los punteros gestionados superiores ahora no es una TypedReference para llevar.

Y, obviamente, la desaparición de TypedReference -o su depreciación casi completa al menos- significa lanzar __makeref al junkheap.


Respuesta corta: portabilidad .

Mientras que __arglist , __makeref y __refvalue son extensiones de lenguaje y no están documentadas en la especificación de lenguaje C #, las construcciones utilizadas para implementarlas bajo el capó (convenciones de llamadas TypedReference , tipo arglist , refanytype , mkanyref , refanyval y refanyval ) están perfectamente documentadas en la Especificación CLI (ECMA-335) en la biblioteca Vararg .

Al estar definido en la biblioteca Vararg, queda bastante claro que están destinados principalmente a admitir listas de argumentos de longitud variable y no mucho más. Las listas de argumentos variables tienen poco uso en las plataformas que no necesitan interconectarse con el código C externo que usa varargs. Por esta razón, la biblioteca de Varargs no es parte de ningún perfil CLI. Las implementaciones de CLI legítimas pueden optar por no admitir la biblioteca de Varargs ya que no está incluida en el perfil del núcleo de la CLI:

4.1.6 Vararg

El conjunto de características vararg admite listas de argumentos de longitud variable y punteros tipeados en tiempo de ejecución.

Si se omite: Cualquier intento de referenciar un método con la convención de llamada vararg o las codificaciones de firma asociadas con los métodos vararg (vea la Partición II) emitirá la excepción System.NotImplementedException . Los métodos que utilizan las instrucciones CIL arglist , refanytype , mkrefany y refanyval arrojarán la excepción System.NotImplementedException . El tiempo exacto de la excepción no se especifica. No es necesario definir el tipo System.TypedReference .

Actualización (respuesta al comentario GetValueDirect ):

FieldInfo.GetValueDirect es FieldInfo.SetValueDirect no es parte de Base Class Library. Tenga en cuenta que existe una diferencia entre .NET Framework Class Library y Base Class Library. BCL es lo único necesario para una implementación conforme de la CLI / C # y está documentado en ECMA TR / 84 . (De hecho, FieldInfo sí mismo es parte de la biblioteca Reflection y no está incluido en el perfil CLI Kernel tampoco).

Tan pronto como utiliza un método fuera de BCL, está renunciando a un poco de portabilidad (y esto se está volviendo cada vez más importante con el advenimiento de implementaciones de CLI que no son de .NET como Silverlight y MonoTouch). Incluso si una implementación desea aumentar la compatibilidad con Microsoft .NET Framework Class Library, podría simplemente proporcionar GetValueDirect y SetValueDirect tomando TypedReference sin hacer que TypedReference manejado especialmente por el tiempo de ejecución (básicamente, haciéndolos equivalentes a sus contrapartidas de object sin el beneficio de rendimiento) )

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

  1. Como cualquier característica, puede convertirse en un obstáculo para las nuevas funciones, especialmente porque esta no encaja realmente en el diseño de C # y requiere extrañas extensiones de sintaxis y un manejo especial de un tipo para el tiempo de ejecución.
  2. Todas las implementaciones de C # tienen que implementar de alguna manera esta característica y no es necesariamente trivial / posible para las implementaciones de C # que no se ejecutan en la parte superior de una CLI o que se ejecutan en la parte superior de una CLI sin Varargs.