Sorpresa de rendimiento con "as" y tipos anulables

330

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 isinstpara tipos anulables es realmente lenta? ¿Es el adicional unbox.anyque 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);
    }
}
Jon Skeet
fuente
8
¿Por qué no mirar el código jitted? Incluso el depurador VS puede mostrarlo.
Anton Tykhyy
2
Solo tengo curiosidad, ¿también probaste con CLR 4.0?
Dirk Vollmar
1
@ Anton: Buen punto. Lo haré en algún momento (aunque esto no está en VS en este momento :) @divo: Sí, y es peor en general. Pero eso está en beta, por lo que puede haber una gran cantidad de código de depuración allí.
Jon Skeet
1
Hoy aprendí que puedes usar asen tipos anulables. Interesante, ya que no se puede usar en otros tipos de valor. En realidad, más sorprendente.
leppie
3
@Lepp tiene mucho sentido que no funcione en tipos de valor. Piénselo, asintenta convertir a un tipo y, si falla, devuelve nulo. No puede establecer los tipos de valor en nulo
Earlz

Respuestas:

209

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í.

Hans Passant
fuente
16
De acuerdo, estoy convencido. Supongo que estoy acostumbrado a pensar que "es" potencialmente costoso debido a las posibilidades de subir una jerarquía de herencia, pero en el caso de un tipo de valor, no hay posibilidad de una jerarquía, por lo que puede ser una simple comparación bit a bit . Todavía creo que el código JIT para el caso anulable podría ser optimizado por el JIT mucho más de lo que es.
Jon Skeet
26

Me parece que isinstes realmente lento en tipos anulables. En el método FindSumWithCastcambié

if (o is int)

a

if (o is int?)

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>
Dirk Vollmar
fuente
1
Es más que eso; en el caso de "conversión", isinstse sigue una prueba de nulidad y luego condicionalmente un unbox.any. En el caso anulable hay un incondicional unbox.any .
Jon Skeet
Sí, resulta que tanto isinst y unbox.anyson más lentos en los tipos anulables.
Dirk Vollmar
@ Jon: Puedes revisar mi respuesta sobre por qué se necesita el elenco. (Sé que esto es viejo, pero acabo de descubrir esto q y pensé que debería proporcionar mi 2c de lo que sé sobre el CLR).
Johannes Rudolph
22

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 asoperador C # emitirá una isinstinstrucción IL (también lo hace el isoperador). (Otra instrucción interesante es que se castclassemite cuando realiza una conversión directa y el compilador sabe que no se puede omitir la comprobación del tiempo de ejecución).

isinstEsto es lo que hace ( ECMA 335 Partition III, 4.6 ):

Formato: isinst typeTok

typeTok es un token de metadatos (a typeref, typedefo 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 seguimiento del verificador) de obj es verificador-asignable al tipo typeTok, entonces isinsttiene éxito y obj (como resultado ) se devuelve sin cambios mientras la verificación rastrea su tipo como typeTok . A diferencia de las coacciones (§1.6) y las conversiones (§3.27), isinstnunca cambia el tipo real de un objeto y conserva la identidad del objeto (ver Partición I).

Entonces, el asesino de rendimiento no es isinsten este caso, sino el adicional unbox.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á un unbox.anydespués de un isinst T?(pero lo omitirá en caso de que lo haga isinst T, cuando Tes un tipo de referencia).

¿Porque hace eso? isinst T?nunca tiene el efecto que hubiera sido obvio, es decir, vuelve a T?. En cambio, todas estas instrucciones aseguran que tengas una "boxed T"que se pueda desempaquetar T?. Para obtener un real T?, aún necesitamos desempaquetar nuestro "boxed T"to T?, por lo que el compilador emite un unbox.anyafter isinst. Si lo piensas bien, esto tiene sentido porque el "formato de caja" T?es solo una "boxed T"y crear castclassy isinstrealizar 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

Cuando se aplica a la forma en caja de un tipo de valor, la unbox.anyinstrucción extrae el valor contenido en obj (de tipo O). (Es equivalente a unboxseguido de ldobj.) Cuando se aplica a un tipo de referencia, la unbox.anyinstrucción tiene el mismo efecto que castclasstypeTok.

(ECMA 335 Partición III, 4.32): unbox

Por lo general, unboxsimplemente 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>valores se convierten en recuadro Tsdurante la operación de recuadro, 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.

Johannes Rudolph
fuente
Creo que la última oración citada podría tener un error tipográfico; ¿No debería "... en el montón ..." estar "en la pila de ejecución ?" Parece que desempaquetar nuevamente en alguna nueva instancia de almacenamiento dinámico de GC cambia el problema original por uno nuevo casi idéntico.
Glenn Slayden
19

Curiosamente, transmití comentarios sobre el soporte del operador al dynamicser un orden de magnitud más lento para Nullable<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) nullestructuras no anulables, lo divide 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;
    }
}
Marc Gravell
fuente
Yowser Esa es una diferencia realmente dolorosa. Eek
Jon Skeet
Si no ha salido nada bueno de todo esto, me ha llevado a incluir advertencias tanto para mi código original como para este :)
Jon Skeet
Sé que esta es una vieja pregunta, pero ¿podría explicar qué quiere decir con "los puntos JIT (y elimina) nullpara estructuras no anulables"? ¿Quiere decir que se reemplaza nullcon un valor predeterminado o algo durante el tiempo de ejecución?
Justin Morgan
2
@Justin: se puede utilizar un método genérico en tiempo de ejecución con cualquier cantidad de permutaciones de parámetros genéricos ( Tetc.). 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.
Marc Gravell
La prueba anulable no restringida sigue siendo 2.5 órdenes de magnitud más lenta, pero hay alguna optimización cuando no se usa la countvariable. Agregar Console.Write(count.ToString()+" ");después de la watch.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 cuando nullse pasa, confirmando que el código original no está realmente haciendo la verificación nula e incrementando para las otras pruebas. Linqpad
Mark Hurd
12

Este es el resultado de FindSumWithAsAndHas arriba: texto alternativo

Este es el resultado de FindSumWithCast: texto alternativo

Recomendaciones:

  • Utilizando as, prueba primero si un objeto es una instancia de Int32; debajo del capó que está utilizando isinst Int32(que es similar al código escrito a mano: if (o es int)). Y usando as, también desempaqueta incondicionalmente el objeto. Y es un verdadero asesino de rendimiento llamar a una propiedad (sigue siendo una función oculta), IL_0027

  • Usando cast, prueba primero si el objeto es un int if (o is int); debajo del capó que está usando isinst Int32. Si se trata de una instancia de int, puede desempaquetar el valor, IL_002D

En pocas palabras, este es el pseudocódigo del uso del asenfoque:

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)

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 un int. No se puede decir lo mismo al usar un asenfoque.

Michael Buen
fuente
11

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 ...

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}

se convierte simplemente ...

if (o is int x)
{
    // ...use x in here
}

He descubierto que un uso común para la nueva sintaxis es cuando está escribiendo un tipo de valor .NET (es decir, structen C # ) que implementa IEquatable<MyStruct>(como la mayoría debería). Después de implementar el Equals(MyStruct other)método fuertemente tipado , ahora puede redirigir con gracia la Equals(Object obj)anulación sin tipo (heredada de Object) de la siguiente manera:

public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);

 


Apéndice: La Releaseacumulació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 la unboxoperación por 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 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').

Glenn Slayden
fuente
9

Perfilado adicional:

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, es entonces fundido enfoque es mucho más rápido que lo enfoque. 303 vs 3524
  • Segundo, .Value es marginalmente más lento que el casting. 3524 vs 3272
  • Tercero, .HasValue es marginalmente más lento que usar has manual (es decir, usar is ). 3524 vs 3282
  • En cuarto lugar, haciendo una comparación de manzana a manzana (es decir, tanto la asignación de HasValue simulado y la conversión simulada Valor sucede juntos) entre simulado como y real como enfoque, podemos ver simulado como sigue siendo significativamente más rápido que el real . 395 vs 3524
  • Por último, basada en la primera y cuarta conclusión, hay algo de malo en que la aplicación ^ _ ^
Michael Buen
fuente
8

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, que no explicará completamente el problema, pero puede contribuir.

James Black
fuente
1
No, corrí eso y es marginalmente más lento.
Henk Holterman
2
Declarar una variable en un lugar diferente solo afecta significativamente el código generado cuando se captura la variable (en ese momento afecta la semántica real) en mi experiencia. Tenga en cuenta que no está creando un nuevo objeto en el montón, aunque ciertamente está creando una nueva instancia de int?en la pila usando unbox.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.
Jon Skeet
Estaba pensando que el elenco probablemente está optimizado ya que ha existido durante tanto tiempo.
James Black el
1
is / cast es un objetivo fácil para la optimización, es un idioma muy molesto.
Anton Tykhyy
44
Las variables locales se asignan en la pila cuando se crea el marco de la pila para el método, por lo que el lugar donde declara la variable en el método no hace ninguna diferencia. (A menos que esté cerrado, por supuesto, pero ese no es el caso aquí.)
Guffa
8

Probé la construcción de verificación de tipo exacto

typeof(int) == item.GetType(), que funciona tan rápido como la item is intversión y siempre devuelve el número (énfasis: incluso si escribió un Nullable<int>a en la matriz, necesitaría usarlo typeof(int)). También necesita un null != itemcheque adicional aquí.

sin embargo

typeof(int?) == item.GetType()permanece rápido (en contraste con item 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/ashacer 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 HasValuecompra? 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?)nullen la matriz, la verificación de tipo devolverá falso.

dalo
fuente
Interesante ... la idea de usar "como" + HasValue (no es más HasValue, nota) es que solo realiza la verificación de tipo una vez en lugar de dos. Está haciendo el "check and unbox" en un solo paso. Parece que debería ser más rápido ... pero claramente no lo es. No estoy seguro de lo que quieres decir con la última oración, pero no existe un recuadro int?: si encajonas un int?valor, termina en un recuadro int o una nullreferencia.
Jon Skeet
7
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

[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

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
Michael Buen
fuente
¿Qué versión de framework estás usando, por interés? Los resultados en mi netbook (usando .NET 4RC) son aún más dramáticos: las versiones que usan As son mucho peores que sus resultados. ¿Tal vez lo han mejorado para .NET 4 RTM? Todavía creo que podría ser más rápido ...
Jon Skeet
@ Michael: ¿Estaba ejecutando una compilación no optimizada o ejecutándose en el depurador?
Jon Skeet
@ Jon: construcción no optimizada, bajo depurador
Michael Buen
1
@Michael: Correcto: tiendo a ver los resultados de rendimiento bajo un depurador como irrelevantes :)
Jon Skeet
@ Jon: si por debajo del depurador, lo que significa dentro de VS; sí, el punto de referencia anterior se realizó bajo depurador. Comparo de nuevo, dentro de VS y fuera de él, y compilado como depuración y compilado como lanzamiento. Mira la edición
Michael Buen