Pregunta

Sólo estoy revisando el capítulo 4 de C# en Profundidad de que trata con tipos que aceptan valores null, y voy a agregar una sección sobre el uso del "como" el operador, lo que 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 a través de la C# 1 equivalente, utilizando el "es", seguido por un elenco - después de todo, de esta forma solo tenemos que pedir la comprobación de tipo dinámico de una vez, y luego una simple verificación de valor.

Este no parece ser el caso, sin embargo.He incluido un examen de ejemplo de la aplicación a continuación, que básicamente resume todos los enteros dentro de un array de objetos - pero la matriz contiene una gran cantidad de referencias nulas y cadena de referencias, así como cajas de números enteros.El índice de referencia mide el código que tendría que usar en C# 1, el código que utiliza el "como" del operador, y para probar un LINQ solución.Para mi asombro, el C# 1 código es 20 veces más rápido en este caso - y el LINQ de código (que me hubiera esperado a ser más lento, dado el iteradores involucrados) vence el "como" de código.

Es el .NETO de la aplicación de isinst para los tipos que aceptan valores null muy lento?Es el adicional unbox.any que causa el problema?Hay otra explicación para esto?En el momento en que se siente como que voy a tener que incluir una advertencia contra el uso de este en el rendimiento sensible a las situaciones...

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);
    }
}
¿Fue útil?

Solución

Es evidente que el código de máquina del compilador JIT puede generar para el primer caso es mucho más eficiente. Una regla que realmente ayuda es que hay un objeto sólo puede ser sin embalaje a una variable que tiene el mismo tipo que el valor en caja. Eso permite que el compilador JIT para generar código muy eficiente, sin conversiones de valores tienen que ser considerados.

El es prueba operador es 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. El reparto también es fácil, el compilador JIT conoce la ubicación de los bits de valor en el objeto y los utiliza directamente. Prohibida la reproducción o la conversión se produce, todo el código máquina es en línea y toma sino de una docena de instrucciones. Esto tenía que estar de vuelta muy eficiente en .NET 1.0, cuando el boxeo era común.

Conversión a int? requiere mucho más trabajo. La representación 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 CLR ayudante llamado JIT_Unbox_Nullable a hacer el trabajo. Esta es una función de propósito general para cualquier tipo de valor, una gran cantidad de código allí para comprobar tipos. Y el valor se copia. Es difícil estimar el costo ya que este código está encerrado dentro mscorwks.dll, pero cientos de instrucciones de código máquina es probable.

El método de extensión Linq OfType () también utiliza el es operador y el reparto. 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 reparto de Nullable<int>, teniendo en cuenta que menos trabajo debería ser necesario. Sospecho que puede provocar errores en ngen.exe problemas aquí.

Otros consejos

Me parece que el isinst es simplemente muy lento en los tipos anulables. En FindSumWithCast método cambié

if (o is int)

a

if (o is int?)

que también reduce de forma considerable la ejecución. La única differenc en IL I puede ver es que

isinst     [mscorlib]System.Int32

se cambia a

isinst     valuetype [mscorlib]System.Nullable`1<int32>

Esta originalmente comenzó como un comentario a la excelente respuesta de Hans Passant, pero se hacía demasiado largo, así que quiero añadir unos trocitos aquí:

En primer lugar, el C operador # as emitirá una instrucción IL isinst (también lo hace el operador is). (Otra instrucción interesante es castclass, emite cuando usted hace un reparto directo y el compilador sabe que la comprobación en tiempo de ejecución no se pueden omitir recayendo.)

Esto es lo que hace isinst ( ECMA 335 Partición III, 4,6 ):

  

Formato: isinst typeTok

     

typeTok es un metadatos token (un typeref, typedef o typespec), lo 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 “caja” typeTok .

     

Si typeTok es un tipo anulable, Nullable<T>, se interpreta como “caja” T

Lo más importante:

  

Si el tipo real (no el tipo verificador rastreado) de obj es verificador asignable a el tipo typeTok entonces isinst tiene éxito y obj (como resultado ) se devuelve sin cambios mientras que la verificación de seguimiento de su tipo como typeTok . coacciones diferencia (§ 1.6) y conversiones (§3.27), isinst nunca cambia el tipo real de un objeto y conservas objeto de identidad (véase la partición I).

Por lo tanto, el asesino rendimiento no es isinst en este caso, pero el unbox.any adicional. Esto no estaba claro de la respuesta de Hans, al mirar sólo el código JITed. En general, el compilador de C # emitirá una unbox.any después de un isinst T? (pero se lo omite en caso de que isinst T, cuando T es un tipo de referencia).

¿Por qué lo hace? isinst T? no tiene el efecto que habría sido obvio, es decir, que a volver un T?. En su lugar, todas estas instrucciones asegurar es que usted tiene una "boxed T" que puede ser sin embalaje a T?. Para tener una T? real, todavía tenemos que desempacar nuestra "boxed T" a T?, por lo que el compilador emite una unbox.any después isinst. Si se piensa en ello, esto tiene sentido porque el "formato de caja" para T? es sólo un "boxed T" y haciendo castclass y isinst realizan la unbox sería incompatible.

Copia de seguridad de Hans encontrar con un poco de información de la 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, los extractos de instrucciones unbox.any el valor contenido dentro obj (de tipo O). (Es equivalente a unbox seguido por ldobj.) Cuando se aplica a un tipo de referencia, la instrucción unbox.any tiene el mismo efecto que typeTok castclass.

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

  

Típicamente, unbox simplemente calcula la dirección del tipo de valor que ya está presente en el interior del objeto en caja. Este enfoque no es posible cuando unboxing tipos de valores con valores nulos. Dado que los valores Nullable<T> se convierten a Ts caja 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.

Curiosamente, pasé en la retroalimentación sobre el apoyo del operador a través dynamic ser una magnitud de orden de más lento para Nullable<T> (similar a esta prueba temprana ) - sospecho por razones muy similares

. amor Nullable<T>

Obligatorio. Otra diversión es que a pesar de los puntos (JIT) y elimina null de estructuras no anulable, se Borks 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: alt text

Este es el resultado de FindSumWithCast: alt text

Conclusiones:

  • El uso de as, prueba primero si un objeto es una instancia de Int32;bajo el capó es el uso de isinst Int32 (que es similar a el código a mano:si (o es de tipo int) ).Y el uso de as, también incondicionalmente unbox el objeto.Y es un verdadero rendimiento-killer para llamar a una propiedad(es todavía una función bajo el capó), IL_0027

  • El uso de yeso, se prueba primero si el objeto es un int if (o is int);bajo el capó es el uso de isinst Int32.Si es una instancia de tipo int, a continuación, puede unbox el valor, IL_002D

En pocas palabras, este es el pseudo-código de la utilización de as enfoque:

int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    

Y este es el pseudo-código de la utilización de fundición de enfoque:

if (o isinst Int32)
    sum += (o unbox Int32)

Así que el elenco ((int)a[i], bien , el aspecto de la sintaxis de un yeso, pero en realidad unboxing, el reparto y unboxing comparten la misma sintaxis, la próxima vez voy a ser pedante con la terminología correcta), el método es mucho más rápido, usted sólo necesita a unbox un valor cuando un objeto es decididamente un int.Lo mismo no puede decirse para el uso de un as enfoque.

Perfilado ademá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 deducir de estas cifras?

  • En primer lugar, es entonces fundido enfoque es significativamente más rápido que como enfoque. 303 vs 3524
  • En segundo lugar, .Value es ligeramente más lento que el de fundición. 3524 vs 3272
  • En tercer lugar, .HasValue es marginalmente más lento que usar el manual tiene (es decir. Utilizando es ). 3524 vs 3282
  • En cuarto lugar, haciendo una comparación de manzanas con manzanas (es decir, tanto la asignación de HasValue simulado y la conversión simulada Valor sucede juntos) entre simulado como y real enfoque, puede ver simulado como es aún significativamente más rápido que real . 395 vs 3524
  • Por último, basada en la primera y cuarta conclusión, hay algo malo en como implementación ^ _ ^

Con el fin de mantener esta respuesta hasta a la fecha, vale la pena mencionar que la mayor parte de la discusión en esta página es ahora irrelevante ahora con C # 7.1 y .NET 4.7 que soporta una sintaxis delgado que también produce el mejor código IL.

ejemplo original de la 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 encontrado que un uso común para la nueva sintaxis es cuando se está escribiendo un .NET Tipo de valor (es decir struct en C # ) que implementa IEquatable<MyStruct> (ya que la mayoría debería). Después de implementar el método Equals(MyStruct other) inflexible de tipos, ahora se puede redirigir con gracia la anulación Equals(Object obj) sin tipo (heredado de Object) de la siguiente manera:

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


Apéndice: La acumulación Release IL código para las dos primeras funciones ejemplo mostrado arriba en esta respuesta (respectivamente) se dan aquí. Mientras que el código IL para la nueva sintaxis es de hecho 1 byte más pequeño, sobre todo gana a lo grande al hacer llamadas a cero (frente a dos) y evitar la operación unbox 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 obtener más pruebas que demuestren mi observación sobre el rendimiento de la nueva C # 7 sintaxis superando las opciones disponibles anteriormente, ver aquí (en particular, el ejemplo de D ').

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?;

Usted está creando un nuevo objeto cada vez, que no se explican por completo el problema, pero pueden contribuir.

He probado el tipo exacto de verificación construir

typeof(int) == item.GetType(), que realiza tan rápido como el item is int versión, y siempre devuelve el número (énfasis:incluso si usted escribió un Nullable<int> a la matriz, usted tendrá que usar la typeof(int)).También se necesita un adicional null != item marque aquí.

Sin embargo

typeof(int?) == item.GetType() estancias rápido (en contraste con item is int?), pero siempre devuelve false.

El tipo de-construir, en mis ojos la manera más rápida para exacto la comprobación de tipos, ya que utiliza el RuntimeTypeHandle.Dado que los tipos exactos en este caso no coinciden con los que aceptan valores null, supongo, is/as tienes que hacer adicional heavylifting aquí en asegurar que es en realidad una instancia de un tipo que acepta valores null.

Y sinceramente:¿qué hace su is Nullable<xxx> plus HasValue comprar a usted?Nada.Siempre se puede ir directamente a la base (valor) tipo (en este caso).Usted obtener el valor o "no, no es una instancia del tipo que estaban pidiendo".Incluso si usted escribió (int?)null a la matriz, el tipo de verificación devolverá false.

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 llevó a cabo dentro de VS, de depuración de configuración, usando VS2009, utilizando Core i7 (equipo de desarrollo de la empresa).

A continuación se llevó a cabo en mi máquina utilizando 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
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top