type number generic convert cast c# performance clr nullable unboxing

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á utilizando isinst Int32 (que es similar al código escrito a mano: if (o es int)). Y usando as , 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_0027

  • Usando cast, primero prueba si el objeto es un int if (o is int) ; bajo el capó esto está usando isinst 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 o typespec ), 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 tipo O ). (Es equivalente a unbox seguido de ldobj .) Cuando se aplica a un tipo de referencia, la instrucción unbox.any tiene el mismo efecto que castclass 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 los Nullable<T> se convierten a Ts durante la operación de la caja, una implementación a menudo debe fabricar un nuevo Nullable<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