generic - type number c#
Sorpresa de rendimiento con tipos "as" y anulables. (10)
Claramente, el código de máquina que puede generar el compilador JIT para el primer caso es mucho más eficiente. Una regla que realmente ayuda allí es que un objeto solo se puede desempaquetar en una variable que tiene el mismo tipo que el valor en caja. Eso permite que el compilador JIT genere un código muy eficiente, no se deben considerar conversiones de valor.
La prueba del operador es fácil, solo verifique si el objeto no es nulo y es del tipo esperado, solo toma unas pocas instrucciones de código de 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 utiliza directamente. No se realiza ninguna copia o conversión, todo el código de la máquina está en línea y toma alrededor de una docena de instrucciones. Esto necesitaba ser realmente eficiente en .NET 1.0 cuando el boxeo era común.
¿Casting a int? Se necesita 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 de CLR llamada JIT_Unbox_Nullable para realizar el trabajo. Esta es una función de propósito general para cualquier tipo de valor, un montón de código para verificar 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 cast. Sin embargo, esto es una conversión 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 de Nullable<int>
, dado que debería ser necesario menos trabajo. Sospecho que ngen.exe podría causar problemas aquí.
Solo estoy revisando el capítulo 4 de C # en profundidad, que trata sobre tipos que admiten nulos, 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 en comparación con el equivalente de C # 1, usando "es" seguido de una conversión - después de todo, de esta manera solo necesitamos solicitar la comprobación dinámica de tipos una vez, y luego una simple comprobación de valor .
Esto parece no ser el caso, sin embargo. He incluido una aplicación de prueba de muestra a continuación, que básicamente resume todos los enteros dentro de una matriz de objetos, pero la matriz contiene muchas referencias nulas y referencias de cadena, así como también 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 "as" y solo para 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 habría esperado que fuera más lento, dados los iteradores involucrados) supera el código "como".
¿La implementación de .NET de isinst
para tipos anulables es realmente lenta? ¿Es el unbox.any
adicional que causa el problema? ¿Hay otra explicación para esto? En este momento, siento 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);
}
}
Curiosamente, transmití comentarios sobre el soporte del operador a través de dynamic
es un orden de magnitud más lento para Nullable<T>
(similar a esta prueba inicial ). Sospecho que por razones muy similares.
Tengo que amar a Nullable<T>
. Otra diversión es que, aunque el JIT detecta (y elimina) el null
de las estructuras no anulables, lo hace para Nullable<T>
:
using System;
using System.Diagnostics;
static class Program {
static void Main() {
// JIT
TestUnrestricted<int>(1,5);
TestUnrestricted<string>("abc",5);
TestUnrestricted<int?>(1,5);
TestNullable<int>(1, 5);
const int LOOP = 100000000;
Console.WriteLine(TestUnrestricted<int>(1, LOOP));
Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
Console.WriteLine(TestNullable<int>(1, LOOP));
}
static long TestUnrestricted<T>(T x, int loop) {
Stopwatch watch = Stopwatch.StartNew();
int count = 0;
for (int i = 0; i < loop; i++) {
if (x != null) count++;
}
watch.Stop();
return watch.ElapsedMilliseconds;
}
static long TestNullable<T>(T? x, int loop) where T : struct {
Stopwatch watch = Stopwatch.StartNew();
int count = 0;
for (int i = 0; i < loop; i++) {
if (x != null) count++;
}
watch.Stop();
return watch.ElapsedMilliseconds;
}
}
Este es el resultado de FindSumWithAsAndHas arriba: texto alternativo http://www.freeimagehosting.net/uploads/9e3c0bfb75.png
Este es el resultado de FindSumWithCast: texto alt http://www.freeimagehosting.net/uploads/ce8a5a3934.png
Recomendaciones:
Usando
as
, prueba primero si un objeto es una instancia de Int32; bajo el capó está utilizandoisinst Int32
(que es similar al código escrito a mano: if (o es int)). Y usandoas
, también incondicionalmente unboxea el objeto. Y es un verdadero asesino de rendimiento llamar a una propiedad (aún es una función bajo el capó), IL_0027Usando cast, primero prueba si el objeto es un
int
if (o is int)
; bajo el capó esto está usandoisinst Int32
. Si es una instancia de int, entonces puede desempaquetar el valor de forma segura, IL_002D
En pocas palabras, este es el pseudocódigo de usar as
enfoque:
int? x;
(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)
if (x.HasValue)
sum += x.Value;
Y este es el pseudocódigo del uso del enfoque de conversión:
if (o isinst Int32)
sum += (o unbox Int32)
Así que el elenco ( (int)a[i]
, bueno, la sintaxis parece un elenco, 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 necesitabas unbox un valor cuando un objeto es decididamente un int
. No se puede decir lo mismo que usar un enfoque as
.
Esto originalmente 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 C # as
operador emitirá una instrucción isinst
IL (también lo hace el operador). (Otra instrucción interesante es castclass
, que se emite cuando se realiza una conversión directa y el compilador sabe que la verificación del tiempo de ejecución no se puede omitir).
isinst
es lo que hace isinst
( ECMA 335 Partition III, 4.6 ):
Formato: isinst typeTok
typeTok es un token de metadatos (
typeref
,typedef
otypespec
), que indica la clase deseada.Si typeTok es un tipo de valor no anulable o un tipo de parámetro genérico, se interpreta como typeTok "en caja".
Si typeTok es un tipo anulable,
Nullable<T>
, se interpreta como "en caja"T
Más importante:
Si el tipo real (no el tipo de verificador rastreado) de obj es verificador-asignable-al tipo typeTok, entonces
isinst
tiene éxito y obj (como resultado ) se devuelve sin cambios mientras la verificación rastrea su tipo como typeTok . A diferencia de las coerciones (§1.6) y las conversiones (§3.27),isinst
nunca cambia el tipo real de un objeto y conserva la identidad del objeto (consulte la Partición I).
Por lo tanto, el asesino de rendimiento no es isinst
en este caso, pero unbox.any
adicional. Esto no quedó claro en la respuesta de Hans, ya que solo miró el código JITed. En general, el compilador de C # emitirá un unbox.any
después de un isinst T?
(pero lo omitiré en caso de que lo haga isinst T
, cuando T
es un tipo de referencia).
¿Porque hace eso? isinst T?
¿Nunca ha tenido el efecto que hubiera sido obvio, es decir, recuperar una T?
. En su lugar, todas estas instrucciones aseguran que tiene una "boxed T"
que se puede desempaquetar en T?
. Para obtener una T?
real T?
, todavía tenemos que desempaquetar nuestra "boxed T"
a T?
, que es la razón por la cual el compilador emite un unbox.any
después de isinst
. Si lo piensas bien, esto tiene sentido porque el "formato de caja" para T?
es solo una "boxed T"
y hacer que castclass
e isinst
realicen la unbox sería inconsistente.
Respaldando el hallazgo de Hans con información del estándar , aquí va:
(ECMA 335 Partition III, 4.33): unbox.any
Cuando se aplica a la forma en caja de un tipo de valor, la instrucción
unbox.any
extrae el valor contenido dentro de obj (de tipoO
). (Es equivalente aunbox
seguido deldobj
.) Cuando se aplica a un tipo de referencia, la instrucciónunbox.any
tiene el mismo efecto quecastclass
typeTok.
(ECMA 335 Partition III, 4.32): unbox
Normalmente,
unbox
simplemente calcula la dirección del tipo de valor que ya está presente dentro del objeto encuadrado. Este enfoque no es posible al desempaquetar tipos de valores anulables. Debido a que losNullable<T>
se convierten aTs
durante la operación de la caja, una implementación a menudo debe fabricar un nuevoNullable<T>
en el montón y calcular la dirección al objeto recién asignado.
Me parece que el isinst
es realmente lento en tipos anulables. En el método FindSumWithCast
cambié
if (o is int)
a
if (o is int?)
lo que también ralentiza significativamente la ejecución. La única diferencia en IL que puedo ver es que
isinst [mscorlib]System.Int32
se cambia a
isinst valuetype [mscorlib]System.Nullable`1<int32>
No tengo tiempo para probarlo, pero es posible que desee tener:
foreach (object o in values)
{
int? x = o as int?;
como
int? x;
foreach (object o in values)
{
x = o as int?;
Está creando un nuevo objeto cada vez, lo que no explicará completamente el problema, pero puede contribuir.
Para mantener esta respuesta actualizada, vale la pena mencionar que la mayor parte de la discusión en esta página ahora es discutible 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 ...
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
// ...use x.Value in here
}
se convierte simplemente en ...
if (o is int x)
{
// ...use x in here
}
Descubrí que un uso común de la nueva sintaxis es cuando se escribe un tipo de valor .NET (es decir, struct
en C # ) que implementa el IEquatable<MyStruct>
(como la mayoría debería). Después de implementar el método Equals(MyStruct other)
fuertemente tipado, ahora puede redirigir con gracia la anulación Equals(Object obj)
(heredada de Object
) de la siguiente manera:
public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
Apéndice: El código IL de compilación de la Release
para las dos primeras funciones de ejemplo que se muestran 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 haciendo cero llamadas (frente a dos) y evitando la operación unbox
completo cuando sea posible.
// static void test1(Object o, ref int y)
// {
// int? x = o as int?;
// if (x.HasValue)
// y = x.Value;
// }
[0] valuetype [mscorlib]Nullable`1<int32> x
ldarg.0
isinst [mscorlib]Nullable`1<int32>
unbox.any [mscorlib]Nullable`1<int32>
stloc.0
ldloca.s x
call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
brfalse.s L_001e
ldarg.1
ldloca.s x
call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
stind.i4
L_001e: ret
// static void test2(Object o, ref int y)
// {
// if (o is int x)
// y = x;
// }
[0] int32 x,
[1] object obj2
ldarg.0
stloc.1
ldloc.1
isinst int32
ldnull
cgt.un
dup
brtrue.s L_0011
ldc.i4.0
br.s L_0017
L_0011: ldloc.1
unbox.any int32
L_0017: stloc.0
brfalse.s L_001d
ldarg.1
ldloc.0
stind.i4
L_001d: ret
Para más pruebas que justifiquen mi comentario sobre el rendimiento de la nueva sintaxis de C # 7 superando las opciones disponibles anteriormente, consulte here (en particular, el ejemplo ''D'').
Probé el tipo exacto de verificación de construcción
typeof(int) == item.GetType()
, que funciona tan rápido como el item is int
versión y siempre devuelve el número (énfasis: incluso si escribió una Nullable<int>
en la matriz, tendría que usar typeof(int)
). También necesita un elemento null != item
adicional null != item
control de null != item
aquí.
sin embargo
typeof(int?) == item.GetType()
mantiene rápido (en contraste con item is int?
), pero siempre devuelve false.
El tipo de construcción es, en mi opinión, la forma más rápida de verificar el tipo exacto , ya que utiliza el RuntimeTypeHandle. Dado que los tipos exactos en este caso no coinciden con los valores anulables, supongo que es, is/as
se debe hacer un trabajo pesado en este punto para asegurarse de que sea en realidad una instancia de un tipo anulable.
Y honestamente: ¿qué es lo que hace is Nullable<xxx> plus HasValue
comprarte? Nada. Siempre puede ir directamente al tipo subyacente (valor) (en este caso). O obtiene el valor o "no, no una instancia del tipo que estaba solicitando". Incluso si escribió (int?)null
en la matriz, la comprobación de tipo devolverá false.
Profiling más:
using System;
using System.Diagnostics;
class Program
{
const int Size = 30000000;
static void Main(string[] args)
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i + 1] = "";
values[i + 2] = 1;
}
FindSumWithIsThenCast(values);
FindSumWithAsThenHasThenValue(values);
FindSumWithAsThenHasThenCast(values);
FindSumWithManualAs(values);
FindSumWithAsThenManualHasThenValue(values);
Console.ReadLine();
}
static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsThenHasThenCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += (int)o;
}
}
sw.Stop();
Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithManualAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
bool hasValue = o is int;
int x = hasValue ? (int)o : 0;
if (hasValue)
{
sum += x;
}
}
sw.Stop();
Console.WriteLine("Manual As: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsThenManualHasThenValue(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (o is int)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
}
Salida:
Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282
¿Qué podemos inferir de estas cifras?
- En primer lugar, el enfoque is-then-cast es significativamente más rápido que el enfoque. 303 vs 3524
- En segundo lugar, el valor es ligeramente más lento que el lanzamiento. 3524 vs 3272
- Tercero, .HasValue es ligeramente más lento que usar el manual (es decir, usar es ). 3524 vs 3282
- Cuarto, hacer una comparación de manzana a manzana (es decir, tanto la asignación de HasValue simulado como la conversión del Valor simulado ocurren juntas) entre el enfoque de simulado y real como real , podemos ver que el simulado es significativamente más rápido que el real . 395 vs 3524
- Por último, según la primera y cuarta conclusión, hay algo malo con la implementación ^ _ ^
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);
FindSumWithAsAndHas(values);
FindSumWithAsAndIs(values);
FindSumWithIsThenAs(values);
FindSumWithIsThenConvert(values);
FindSumWithLinq(values);
Console.ReadLine();
}
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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithAsAndIs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (o is int)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As and Is: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithIsThenAs(object[] values)
{
// Apple-to-apple comparison with Cast routine above.
// Using the similar steps in Cast routine above,
// the AS here cannot be slower than Linq.
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int? x = o as int?;
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("Is then As: {0} : {1}", sum,
(long)sw.ElapsedMilliseconds);
}
static void FindSumWithIsThenConvert(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = Convert.ToInt32(o);
sum += x;
}
}
sw.Stop();
Console.WriteLine("Is then Convert: {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);
}
}
Salidas:
Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811
[EDIT: 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 empresa).
Lo siguiente se hizo en mi máquina usando Core 2 Duo, usando VS2010
Inside VS, Configuration: Debug
Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018
Outside VS, Configuration: Debug
Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944
Inside VS, Configuration: Release
Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932
Outside VS, Configuration: Release
Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936