302 votos

El rendimiento de sorpresa con el "como" y los tipos que aceptan valores null

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

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

Es el .NET 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 si me voy a tener que incluir una advertencia contra el uso de este en el rendimiento delicadas las situaciones en las...

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);
    }
}

195voto

Hans Passant Puntos 475940

Claramente el código máquina el compilador JIT se puede generar para el primer caso es mucho más eficiente. Una regla que realmente ayuda a que un objeto sólo puede ser sin caja a una variable que tiene el mismo tipo de la caja de valor. Que permite que el compilador JIT para generar código eficiente, no tiene valor conversiones han de ser considerados.

El es el operador de la prueba es fácil, basta con comprobar si el objeto no es nula y es del tipo esperado, lleva un par de instrucciones de código de máquina. El elenco también es fácil, el compilador JIT se conoce la ubicación del valor de los bits en el objeto y las utiliza directamente. No copiar o conversión se produce, todos del código de la máquina está en línea y lleva alrededor de una docena de instrucciones. Esta necesarios para ser realmente eficiente en la espalda .NET 1.0 cuando el boxeo era común.

La fundición a la int? lleva mucho más trabajo. El valor de la representación de la caja entero no es compatible con el diseño de memoria Nullable<int>. Una es necesaria la conversión y el código es difícil debido a la posible caja tipo enum. El compilador JIT se genera una llamada a un CLR función auxiliar denominado JIT_Unbox_Nullable para hacer el trabajo. Esta es una función de propósito general para cualquier tipo de valor, un montón de código que hay a la verificación de tipos. Y el valor es copiado. Difícil estimar el coste, ya que este código es encerrado dentro de mscorwks.dll pero cientos de instrucciones de código de máquina es probable.

El Linq OfType() método de extensión se utiliza también la es el operador y el elenco. Sin embargo se trata de una conversión a un tipo genérico. El compilador JIT se genera una llamada a una función auxiliar, JIT_Unbox() que puede realizar un molde para un valor arbitrario tipo. No tengo una gran explicación de por qué es tan lento como el yeso Nullable<int>, dado que menos debería ser necesario. Tengo la sospecha de que ngen.exe podría causar problemas aquí.

23voto

0xA3 Puntos 73439

A mí me parece que el isinst es realmente lento en tipos que aceptan valores null. En el método FindSumWithCast he cambiado

if (o is int)

a

if (o is int?)

que también reduce significativamente la ejecución. La única differenc en IL puedo ver es que

isinst     [mscorlib]System.Int32

se cambia a

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

22voto

Johannes Rudolph Puntos 19845

Este originalmente comenzó como un Comentario de Hans Passants excelente respuesta, pero lo tengo muy largo así que quiero añadir un par de detalles aquí:

En primer lugar, el de C# como operador emitirá una isinst de ALFIN (lo hace el operador). (Nota: el otro interesante instrucción es castclass, emitido al hacer un directo elenco y el compilador sabe que el tiempo de ejecución de la comprobación no se puede no se incluye)..

Aquí está lo que se hace (ECMA 335 Partición III, 4.6):

Formato: isinst typeTok

  • typeTok es una de metadatos token (una typeref, typedef o typespec), indica la clase deseada. Si
  • typeTok es una que no admite valores null tipo de valor o genérica de un tipo de parámetro que se interpreta como-en caja‖ typeTok. Si
  • typeTok es un tipo que acepta valores null, acepta valores null, se interpreta como -caja‖ T.

Lo más importante:

Si el tipo real (no el comprobador de seguimiento) el tipo de obj es verificador asignable-el tipo typeTok luego isinst tiene éxito y obj (como resultado) se devuelve sin cambios, mientras que la verificación de las pistas de su tipo como typeTok. A diferencia de coacciones (§1.6) y las conversiones (§3.27), isinst nunca cambia el tipo real de un objeto y el objeto conserva identidad (ver I Partición).

Así, el desempeño asesino no isinst en este caso, pero el adicional unbox.any. Esto no estaba claro de Hans Contestar, miró el JITed sólo código. En general, el compilador de C# se emiten unbox.any después isinst T? (pero se omite en el caso de que isinst T, cuando T es un tipo de referencia).

¿Por qué hace eso? isinst T? nunca tiene el efecto de que habría sido evidente, es decir. obtiene un T?. En su lugar, todas estas instrucciones para asegurarse de que usted tiene un "boxed T" que puede ser sin caja para T?. Para obtener un real T?, todavía tenemos que unbox nuestro "boxed T" a T?, por lo que el compilador emite un unbox.any después de isinst. Si usted piensa acerca de ello, esto tiene sentido porque el cuadro "formato" para T? es solo un "boxed T" y hacer castclass y isinst realizar el unbox sería inconsistente.

Copia de seguridad de Hans encontrar con algo de información de la Norma, aquí va:

(ECMA 335 Partición III, 4.33): unbox.any

Cuando se aplica a la caja en forma de un tipo de valor, el unbox.any la instrucción extrae el valor contenido dentro de obj (de tipo O). ( es equivalente a unbox seguido por ldobj.) Cuando se aplica a una referencia tipo, la unbox.any instrucción tiene el mismo efecto que castclass typeTok.

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

[Nota: Normalmente, unbox simplemente calcula la dirección del tipo de valor que ya está presente en el interior de la caja de objetos. Este enfoque es no es posible al unboxing que aceptan valores null tipos de valor. Debido A Que Aceptan Valores Null los valores se convierten en caja de Ts durante la operación de caja, una aplicación a menudo se debe fabricar un nuevo aceptan valores null en el montón y calcular la dirección del objeto recién asignado. nota final]

19voto

Marc Gravell Puntos 482669

Curiosamente, yo pasé comentarios sobre el apoyo de los operadores a través de dynamic ser un orden de magnitud más lento para Nullable<T> (similar a la de este examen) - sospecho que por muy similares razones.

Me encantan Nullable<T>. Otro divertido es que aunque el JIT (manchas y elimina) null para que no admite valores null structs, 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;
    }
}

12voto

Michael Buen Puntos 20453

Este es el resultado de FindSumWithAsAndHas arriba: alt text

Este es el resultado de FindSumWithCast: alt text

Resultados:

  • Utilizando as, prueba primero si un objeto es una instancia de Int32; bajo el capó está utilizando isinst Int32 (que es similar a código escrito por el usuario: si (o es de tipo int) ). Y utilizando 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

  • Utilizando cast, 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 y, a continuación, usted puede de manera segura unbox el valor, IL_002D

En pocas palabras, este es el pseudo-código de la utilización 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], también el aspecto de la sintaxis de un yeso, pero en realidad unboxing, fundición y conversión 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, sólo es necesaria para unbox un valor cuando un objeto es decididamente un int. Lo mismo no puede decirse para el uso de un as enfoque.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X