Advertencia: esta pregunta es un poco herética ... los programadores religiosos siempre respetan las buenas prácticas, por favor no la lean. :)
¿Alguien sabe por qué se desaconseja tanto el uso de TypedReference (implícitamente, por falta de documentación)?
He encontrado excelentes usos para él, como al pasar 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), para cuando necesita un puntero opaco, o para cuando necesita acceder a un elemento de una matriz rápidamente, cuyas especificaciones encuentra 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 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 funcione 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 una versión del método de la sizeof
instrucción, que en ocasiones 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 quiera 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é se desalientan usos como este (por falta de documentación)? ¿Alguna razón de seguridad en particular? Parece perfectamente seguro y verificable si no se mezcla 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 usaba una versión de depuración del código [olvidé cambiarlo para liberarlo], y no presioné al 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
un promedio).
fuente
TypedReference: 203 ticks
,boxing/unboxing: 31 ticks
. No importa lo que intente (incluidas las diferentes formas de cronometrar), el boxeo / unboxing es aún más rápido en mi sistema.int
->DockStyle
). Esta cajas de verdad, y es casi diez veces más lenta.Respuestas:
Respuesta corta: portabilidad .
Mientras
__arglist
,__makeref
y__refvalue
son extensiones del lenguaje y es indocumentado en el C # Language Specification, las construcciones utilizadas para ponerlas en práctica bajo el capó (vararg
convención de llamada,TypedReference
tipo,arglist
,refanytype
,mkanyref
, yrefanyval
las instrucciones) están perfectamente documentado en la especificación CLI (ECMA-335) en la biblioteca Vararg .Estar definido en la Biblioteca Vararg deja 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 plataformas que no necesitan interactuar con el código C externo que usa varargs. Por este motivo, la biblioteca Varargs no forma parte de ningún perfil de CLI. Las implementaciones legítimas de CLI pueden optar por no admitir la biblioteca Varargs, ya que no está incluida en el perfil de Kernel de CLI:
Actualización (respuesta al
GetValueDirect
comentario):FieldInfo.GetValueDirect
sonFieldInfo.SetValueDirect
son no parte de la biblioteca de clases base. Tenga en cuenta que hay una diferencia entre .NET Framework Class Library y Base Class Library. BCL es lo único requerido para una implementación conforme de CLI / C # y está documentado en ECMA TR / 84 . (De hecho,FieldInfo
es parte de la biblioteca Reflection y eso tampoco está incluido en el perfil CLI Kernel).Tan pronto como use 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 no .NET como Silverlight y MonoTouch). Incluso si una implementación quisiera aumentar la compatibilidad con la biblioteca de clases de Microsoft .NET Framework, simplemente podría proporcionar
GetValueDirect
ySetValueDirect
tomar unaTypedReference
sin hacer queTypedReference
el tiempo de ejecución maneje especialmente (básicamente, hacerlas equivalentes a susobject
contrapartes sin el beneficio de rendimiento).Si lo hubieran documentado en C #, habría tenido al menos un par de implicaciones:
fuente
FieldInfo.GetValueDirect
yFieldInfo.SetValueDirect
? Forman parte del BCL, y para usarlos es necesarioTypedReference
, entonces, ¿no obliga esoTypedReference
a estar siempre definido, independientemente de las especificaciones del idioma? (Además, otra nota: incluso si las palabras clave no existieran, mientras existieran las instrucciones, aún podría acceder a ellas emitiendo métodos dinámicamente ... para que mientras su plataforma interopere con las bibliotecas C, puede usarlas, si C # tiene o no las palabras clave.)TypedReference
se documentara solo para un idioma, por ejemplo, C ++ administrado, pero si ningún idioma lo documenta y si nadie realmente puede usarlo, ¿por qué molestarse en definir la función?)[DllImport("...")] void Foo(__arglist);
) y la implementaron en C # para su propio uso. El diseño de la CLI está influenciado por muchos idiomas (las anotaciones "El Estándar Anotado de Infraestructura de Lenguaje Común" demuestran este hecho). Ser un tiempo de ejecución adecuado para tantos idiomas como sea posible, incluidos los imprevistos, definitivamente ha sido un objetivo de diseño (de ahí que nombre) y esta es una característica que, por ejemplo, una implementación hipotética de C administrada probablemente podría beneficiarse.Bueno, no soy Eric Lippert, por lo que no puedo hablar directamente de las motivaciones de Microsoft, pero si tuviera que adivinar, diría que
TypedReference
et al. no están bien documentados porque, francamente, no los necesitas.Cada uso que mencionó para estas características se puede lograr sin ellas, 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 "el objetivo de rendimiento era" más rápido que Java ").
Eso no quiere decir que no se hayan tenido en cuenta ciertas consideraciones de rendimiento. 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 de tipos, también mejoran el rendimiento de manera similar al
TypedReference
evitar el boxeo y el desempaquetado. De hecho, me preguntaba por qué preferirías esto:a esto:
Las compensaciones, como las veo, son que la primera requiere menos JIT (y, por lo tanto, menos memoria), mientras que la segunda es más familiar y, supongo, un poco más rápido (evitando la desreferenciación del puntero).
Llamaría a mis
TypedReference
amigos y detalles de implementación. Usted ha señalado algunos usos interesantes 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.fuente
call()
: es porque el código no siempre es tan coherente: me refería más a un ejemplo más como eseIAsyncResult.State
, donde la introducción de genéricos simplemente no sería factible porque de repente introduciría genéricos para cada clase / método involucrado Sin embargo, +1 para la respuesta ... especialmente para señalar la parte "más rápido que Java". :]TypedReference
probablemente no sufrirá cambios importantes en el corto plazo, dado que FieldInfo.SetValueDirect , que es público y probablemente utilizado por algunos desarrolladores, depende de ello. :)TypedReference
ninguno de esos. (La sintaxis atroz y la falta de manejabilidad general lo descalifican, en mi opinión, de la categoría agradable). Diría que es bueno tenerlo cuando realmente necesita recortar algunos microsegundos aquí y allá. Dicho esto, estoy pensando en un par de lugares en mi propio código que voy a ver ahora mismo, para ver si puedo optimizarlos usando las técnicas que usted señaló.TypedReference
s, pero IIRC, el único lugar donde pude evitar el boxeo en algún lugar fue con los elementos de matrices unidimensionales de primitivas. El ligero beneficio de velocidad aquí no valía la complejidad que agregó a todo el proyecto, así que lo eliminé.delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
una colección de tiposT
podría proporcionar un métodoActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param)
, pero el JITter tendría que crear una versión diferente del método para cada tipo de valorTParam
. El uso de una referencia escrita permitiría que una versión JITted del método funcione con todos los tipos de parámetros.No puedo entender si se supone que el título de esta pregunta es sarcástico: se ha establecido desde hace mucho tiempo que
TypedReference
es el primo lento, hinchado y feo de los punteros administrados 'verdaderos', este último es lo que obtenemos con C ++ / CLIinterior_ptr<T>
, o incluso parámetros tradicionales de referencia (ref
/out
) en C # . De hecho, es bastante difícil hacer queTypedReference
incluso alcance el rendimiento de la línea base con solo usar un número entero para volver a indexar la matriz CLR original cada vez.Los detalles tristes están aquí , pero afortunadamente, nada de esto importa ahora ...
Estas nuevas características del lenguaje proporcionan un soporte destacado de primera clase en C # para declarar, compartir y manipular
CLR
tipos de tipo de referencia administrados verdaderos en situaciones cuidadosamente prescritas.Las restricciones de uso no son más estrictas de lo que se requería anteriormente
TypedReference
(y el rendimiento está literalmente saltando de peor a mejor ), por lo que no veo ningún caso de uso concebible restante en C # paraTypedReference
. Por ejemplo, anteriormente no había forma de persistirTypedReference
en elGC
montón, por lo que lo mismo ocurre con los punteros administrados superiores que ahora no es para llevar.Y, obviamente, la desaparición de
TypedReference
—o su desaprobación casi completa al menos— también significa tirar__makeref
basura.fuente