Solo estoy revisando el capítulo 4 de C # en profundidad, que trata sobre los tipos anulables, y estoy agregando una sección sobre el uso del operador "como", que le permite escribir:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Pensé que esto era realmente bueno, y que podría mejorar el rendimiento sobre el equivalente de C # 1, usando "es" seguido de un reparto; después de todo, de esta manera solo necesitamos pedir una verificación de tipo dinámico una vez, y luego una simple verificación de valor .
Sin embargo, este no parece ser el caso. He incluido una aplicación de prueba de muestra a continuación, que básicamente suma todos los enteros dentro de una matriz de objetos, pero la matriz contiene muchas referencias nulas y referencias de cadenas, así como enteros en caja. El punto de referencia mide el código que tendría que usar en C # 1, el código que usa el operador "como", y solo por una solución LINQ. Para mi sorpresa, el código C # 1 es 20 veces más rápido en este caso, e incluso el código LINQ (que hubiera esperado que fuera más lento, dados los iteradores involucrados) supera el código "como".
¿La implementación de .NET isinst
para tipos anulables es realmente lenta? ¿Es el adicional unbox.any
que causa el problema? ¿Hay otra explicación para esto? Por el momento parece que voy a tener que incluir una advertencia contra el uso de esto en situaciones sensibles al rendimiento ...
Resultados:
Reparto: 10000000: 121
Como: 10000000: 2211
LINQ: 10000000: 2143
Código:
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i+1] = "";
values[i+2] = 1;
}
FindSumWithCast(values);
FindSumWithAs(values);
FindSumWithLinq(values);
}
static void FindSumWithCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int) o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
}
as
en tipos anulables. Interesante, ya que no se puede usar en otros tipos de valor. En realidad, más sorprendente.as
intenta convertir a un tipo y, si falla, devuelve nulo. No puede establecer los tipos de valor en nuloRespuestas:
Claramente, el código de máquina que el compilador JIT puede generar para el primer caso es mucho más eficiente. Una regla que realmente ayuda es que un objeto solo se puede descomprimir en una variable que tiene el mismo tipo que el valor encuadrado. Eso permite que el compilador JIT genere código muy eficiente, no se deben considerar conversiones de valores.
El es prueba de operador está fácil, basta con comprobar si el objeto no es nulo y es del tipo esperado, pero tiene un par de instrucciones de código máquina. La conversión también es fácil, el compilador JIT conoce la ubicación de los bits de valor en el objeto y los usa directamente. No se produce ninguna copia o conversión, todo el código de la máquina está en línea y toma aproximadamente una docena de instrucciones. Esto tenía que ser realmente eficiente en .NET 1.0 cuando el boxeo era común.
Casting a int? Toma mucho más trabajo. La representación del valor del entero en caja no es compatible con el diseño de memoria de
Nullable<int>
. Se requiere una conversión y el código es complicado debido a los posibles tipos de enumeración en caja. El compilador JIT genera una llamada a una función auxiliar CLR llamada JIT_Unbox_Nullable para hacer el trabajo. Esta es una función de propósito general para cualquier tipo de valor, hay un montón de código para verificar los tipos. Y el valor se copia. Es difícil estimar el costo ya que este código está bloqueado dentro de mscorwks.dll, pero es probable que haya cientos de instrucciones de código de máquina.El método de extensión Linq OfType () también usa el operador is y el elenco. Sin embargo, esto es un reparto a un tipo genérico. El compilador JIT genera una llamada a una función auxiliar, JIT_Unbox () que puede realizar una conversión a un tipo de valor arbitrario. No tengo una gran explicación de por qué es tan lento como el elenco
Nullable<int>
, dado que debería ser necesario menos trabajo. Sospecho que ngen.exe podría causar problemas aquí.fuente
Me parece que
isinst
es realmente lento en tipos anulables. En el métodoFindSumWithCast
cambiéa
que también ralentiza significativamente la ejecución. La única diferencia en IL que puedo ver es que
se cambia a
fuente
isinst
se sigue una prueba de nulidad y luego condicionalmente ununbox.any
. En el caso anulable hay un incondicionalunbox.any
.isinst
yunbox.any
son más lentos en los tipos anulables.Originalmente, esto comenzó como un comentario a la excelente respuesta de Hans Passant, pero se hizo demasiado largo, así que quiero agregar algunos bits aquí:
Primero, el
as
operador C # emitirá unaisinst
instrucción IL (también lo hace elis
operador). (Otra instrucción interesante es que secastclass
emite cuando realiza una conversión directa y el compilador sabe que no se puede omitir la comprobación del tiempo de ejecución).isinst
Esto es lo que hace ( ECMA 335 Partition III, 4.6 ):Más importante:
Entonces, el asesino de rendimiento no es
isinst
en este caso, sino el adicionalunbox.any
. Esto no estaba claro por la respuesta de Hans, ya que solo miró el código JITed. En general, el compilador de C # emitirá ununbox.any
después de unisinst T?
(pero lo omitirá en caso de que lo hagaisinst T
, cuandoT
es un tipo de referencia).¿Porque hace eso?
isinst T?
nunca tiene el efecto que hubiera sido obvio, es decir, vuelve aT?
. En cambio, todas estas instrucciones aseguran que tengas una"boxed T"
que se pueda desempaquetarT?
. Para obtener un realT?
, aún necesitamos desempaquetar nuestro"boxed T"
toT?
, por lo que el compilador emite ununbox.any
afterisinst
. Si lo piensas bien, esto tiene sentido porque el "formato de caja"T?
es solo una"boxed T"
y crearcastclass
yisinst
realizar la unbox sería inconsistente.Respaldando el hallazgo de Hans con información del estándar , aquí va:
(ECMA 335 Partición III, 4.33):
unbox.any
(ECMA 335 Partición III, 4.32):
unbox
fuente
Curiosamente, transmití comentarios sobre el soporte del operador al
dynamic
ser un orden de magnitud más lento paraNullable<T>
(similar a esta prueba inicial ), sospecho por razones muy similares.Tengo amor
Nullable<T>
. Otra divertida es que, aunque el JIT detecta (y elimina)null
estructuras no anulables, lo divide paraNullable<T>
:fuente
null
para estructuras no anulables"? ¿Quiere decir que se reemplazanull
con un valor predeterminado o algo durante el tiempo de ejecución?T
etc.). Los requisitos de la pila, etc. dependen de los argumentos (cantidad de espacio de pila para un local, etc.), por lo que obtiene un JIT para cualquier permutación única que implique un tipo de valor. Sin embargo, las referencias son todas del mismo tamaño, así que comparte un JIT. Mientras realiza el JIT por tipo de valor, puede verificar algunos escenarios obvios e intenta eliminar el código inalcanzable debido a cosas como nulos imposibles. No es perfecto, nota. Además, estoy ignorando AOT por lo anterior.count
variable. AgregarConsole.Write(count.ToString()+" ");
después de lawatch.Stop();
en ambos casos ralentiza las otras pruebas justo por debajo de un orden de magnitud, pero la prueba anulable sin restricciones no cambia. Tenga en cuenta que también hay cambios cuando prueba los casos cuandonull
se pasa, confirmando que el código original no está realmente haciendo la verificación nula e incrementando para las otras pruebas. LinqpadEste es el resultado de FindSumWithAsAndHas arriba:
Este es el resultado de FindSumWithCast:
Recomendaciones:
Utilizando
as
, prueba primero si un objeto es una instancia de Int32; debajo del capó que está utilizandoisinst Int32
(que es similar al código escrito a mano: if (o es int)). Y usandoas
, también desempaqueta incondicionalmente el objeto. Y es un verdadero asesino de rendimiento llamar a una propiedad (sigue siendo una función oculta), IL_0027Usando cast, prueba primero si el objeto es un
int
if (o is int)
; debajo del capó que está usandoisinst Int32
. Si se trata de una instancia de int, puede desempaquetar el valor, IL_002DEn pocas palabras, este es el pseudocódigo del uso del
as
enfoque:Y este es el pseudocódigo del uso del enfoque de conversión:
Entonces, el
(int)a[i]
enfoque de conversión ( bueno, la sintaxis se ve como una conversión, pero en realidad es unboxing, cast y unboxing comparten la misma sintaxis, la próxima vez seré pedante con la terminología correcta) el enfoque es realmente más rápido, solo necesitas desempaquetar un valor cuando un objeto es decididamente unint
. No se puede decir lo mismo al usar unas
enfoque.fuente
Para mantener esta respuesta actualizada, vale la pena mencionar que la mayor parte de la discusión en esta página ahora es discutible ahora con C # 7.1 y .NET 4.7, que admite una sintaxis delgada que también produce el mejor código IL.
El ejemplo original del OP ...
se convierte simplemente ...
He descubierto que un uso común para la nueva sintaxis es cuando está escribiendo un tipo de valor .NET (es decir,
struct
en C # ) que implementaIEquatable<MyStruct>
(como la mayoría debería). Después de implementar elEquals(MyStruct other)
método fuertemente tipado , ahora puede redirigir con gracia laEquals(Object obj)
anulación sin tipo (heredada deObject
) de la siguiente manera:Apéndice: La
Release
acumulación de IL código para las dos primeras funciones ejemplo mostrado arriba en esta respuesta (respectivamente) se dan aquí. Si bien el código IL para la nueva sintaxis es de hecho 1 byte más pequeño, en su mayoría gana en grande al hacer cero llamadas (frente a dos) y evitar launbox
operación por completo cuando sea posible.Para más pruebas que corroboren mi observación sobre el rendimiento de la nueva sintaxis C # 7 que supera las opciones disponibles anteriormente, consulte aquí (en particular, el ejemplo 'D').
fuente
Perfilado adicional:
Salida:
¿Qué podemos inferir de estas cifras?
fuente
No tengo tiempo para probarlo, pero es posible que desee tener:
como
Está creando un nuevo objeto cada vez, que no explicará completamente el problema, pero puede contribuir.
fuente
int?
en la pila usandounbox.any
. Sospecho que ese es el problema: supongo que la IL hecha a mano podría vencer ambas opciones aquí ... aunque también es posible que el JIT esté optimizado para reconocer el caso is / cast y solo verificar una vez.Probé la construcción de verificación de tipo exacto
typeof(int) == item.GetType()
, que funciona tan rápido como laitem is int
versión y siempre devuelve el número (énfasis: incluso si escribió unNullable<int>
a en la matriz, necesitaría usarlotypeof(int)
). También necesita unnull != item
cheque adicional aquí.sin embargo
typeof(int?) == item.GetType()
permanece rápido (en contraste conitem is int?
), pero siempre devuelve falso.El typeof-construct es, en mi opinión, la forma más rápida para la verificación exacta de tipos, ya que utiliza el RuntimeTypeHandle. Dado que los tipos exactos en este caso no coinciden con anulables, supongo que es necesario
is/as
hacer levantamiento de pesas adicional aquí para garantizar que de hecho sea una instancia de un tipo anulable.Y honestamente: ¿qué te
is Nullable<xxx> plus HasValue
compra? Nada. Siempre puede ir directamente al tipo subyacente (valor) (en este caso). Obtiene el valor o "no, no es una instancia del tipo que estaba solicitando". Incluso si escribió(int?)null
en la matriz, la verificación de tipo devolverá falso.fuente
int?
: si encajonas unint?
valor, termina en un recuadro int o unanull
referencia.Salidas:
[EDITAR: 2010-06-19]
Nota: La prueba anterior se realizó dentro de VS, depuración de configuración, usando VS2009, usando Core i7 (máquina de desarrollo de la compañía).
Lo siguiente se hizo en mi máquina usando Core 2 Duo, usando VS2010
fuente