C # 'es' el rendimiento del operador

102

Tengo un programa que requiere un rendimiento rápido. Dentro de uno de sus bucles internos, necesito probar el tipo de un objeto para ver si hereda de una determinada interfaz.

Una forma de hacerlo sería con la función de verificación de tipo incorporada de CLR. El método más elegante probablemente sea la palabra clave 'es':

if (obj is ISpecialType)

Otro enfoque sería darle a la clase base mi propia función virtual GetType () que devuelve un valor de enumeración predefinido (en mi caso, en realidad, solo necesito un bool). Ese método sería rápido, pero menos elegante.

Escuché que hay una instrucción IL específicamente para la palabra clave 'es', pero eso no significa que se ejecute rápidamente cuando se traduzca al ensamblado nativo. ¿Alguien puede compartir alguna idea sobre el rendimiento de 'is' versus el otro método?

ACTUALIZACIÓN: ¡ Gracias por todas las respuestas informadas! Parece que hay un par de puntos útiles repartidos entre las respuestas: el punto de Andrew acerca de 'es' realizar un yeso automáticamente es esencial, pero los datos de rendimiento recopilados por Binary Worrier e Ian también son extremadamente útiles. Sería fantástico si se editara una de las respuestas para incluir toda esta información.

JubJub
fuente
2
Por cierto, CLR no le dará la posibilidad de crear su propia función Type GetType (), porque rompe una de las reglas principales de CLR - realmente tipos
abatishchev
1
Er, no estoy completamente seguro de lo que quiere decir con la regla de "tipos verdaderos", pero entiendo que el CLR tiene una función Type GetType () incorporada. Si tuviera que usar ese método, sería con una función de un nombre diferente que devuelve alguna enumeración, por lo que no habría ningún conflicto de nombre / símbolo.
JubJub
3
Creo que abatishchev significaba "seguridad de tipos". GetType () no es virtual para evitar que un tipo mienta sobre sí mismo y, por lo tanto, preserva la seguridad del tipo.
Andrew Hare
2
¿Ha considerado la búsqueda previa y el almacenamiento en caché del tipo de cumplimiento para no tener que hacerlo dentro de los bucles? Parece que cada pregunta de rendimiento siempre tiene un +1 masivo, pero esto me parece una mala comprensión de c #. ¿Es realmente demasiado lento? ¿Cómo? ¿Qué has probado? Obviamente, no mucho dados sus comentarios sobre las respuestas ...
Gusdor

Respuestas:

114

Usar ispuede dañar el rendimiento si, una vez que verifica el tipo, lanza a ese tipo. isen realidad, envía el objeto al tipo que está verificando, por lo que cualquier conversión posterior es redundante.

Si vas a lanzar de todos modos, aquí tienes un mejor enfoque:

ISpecialType t = obj as ISpecialType;

if (t != null)
{
    // use t here
}
Andrew Hare
fuente
1
Gracias. Pero si no voy a lanzar el objeto si el condicional falla, ¿sería mejor usar una función virtual para probar el tipo?
JubJub
4
@JubJub: no. Un error asbásicamente realiza la misma operación que is(es decir, la verificación de tipo). La única diferencia es que luego regresa en nulllugar de false.
Konrad Rudolph
74

Estoy con Ian , probablemente no quieras hacer esto.

Sin embargo, para que lo sepas, hay muy poca diferencia entre los dos, más de 10,000,000 iteraciones

  • La verificación de enumeración llega a 700 milisegundos (aprox.)
  • La comprobación de IS llega a 1000 milisegundos (aprox.)

Personalmente, no solucionaría este problema de esta manera, pero si me viera obligado a elegir un método sería la verificación de IS incorporada, la diferencia de rendimiento no vale la pena considerar la sobrecarga de codificación.

Mis clases base y derivadas

class MyBaseClass
{
    public enum ClassTypeEnum { A, B }
    public ClassTypeEnum ClassType { get; protected set; }
}

class MyClassA : MyBaseClass
{
    public MyClassA()
    {
        ClassType = MyBaseClass.ClassTypeEnum.A;
    }
}
class MyClassB : MyBaseClass
{
    public MyClassB()
    {
        ClassType = MyBaseClass.ClassTypeEnum.B;
    }
}

JubJub: Como se solicitó más información sobre las pruebas.

Ejecuté ambas pruebas desde una aplicación de consola (una compilación de depuración), cada prueba se parece a la siguiente

static void IsTest()
{
    DateTime start = DateTime.Now;
    for (int i = 0; i < 10000000; i++)
    {
        MyBaseClass a;
        if (i % 2 == 0)
            a = new MyClassA();
        else
            a = new MyClassB();
        bool b = a is MyClassB;
    }
    DateTime end = DateTime.Now;
    Console.WriteLine("Is test {0} miliseconds", (end - start).TotalMilliseconds);
}

Al ejecutar la versión, obtengo una diferencia de 60 a 70 ms, como Ian.

Actualización adicional - 25 de octubre de 2012
Después de un par de años de distancia, noté algo sobre esto, el compilador puede optar por omitir bool b = a is MyClassBen la versión porque b no se usa en ninguna parte.

Este código. . .

public static void IsTest()
{
    long total = 0;
    var a = new MyClassA();
    var b = new MyClassB();
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 10000000; i++)
    {
        MyBaseClass baseRef;
        if (i % 2 == 0)
            baseRef = a;//new MyClassA();
        else
            baseRef = b;// new MyClassB();
        //bool bo = baseRef is MyClassB;
        bool bo = baseRef.ClassType == MyBaseClass.ClassTypeEnum.B;
        if (bo) total += 1;
    }
    sw.Stop();
    Console.WriteLine("Is test {0} miliseconds {1}", sw.ElapsedMilliseconds, total);
}

. . . muestra consistentemente que el ischeque llega a aproximadamente 57 milisegundos, y la comparación de enumeración llega a 29 milisegundos.

Nota : prefiero el ischeque, la diferencia es demasiado pequeña como para preocuparse

Preocupado binario
fuente
35
+1 por probar realmente el rendimiento, en lugar de asumir.
Jon Tackabury
3
Es mucho mejor hacer la prueba con la clase de cronómetro, en lugar de DateTime
Ahora
2
Lo tomaré en cuenta, sin embargo, en este caso, no creo que afecte el resultado. Gracias :)
Binary Worrier
11
@Binary Worrier: las asignaciones de clases de su nuevo operador eclipsarán por completo cualquier diferencia de rendimiento en las operaciones 'is'. ¿Por qué no elimina esas nuevas operaciones, reutilizando dos instancias preasignadas diferentes, y luego vuelve a ejecutar el código y publica los resultados?
1
@mcmillab: Te garantizo que hagas lo que hagas, tendrás cuellos de botella muchos órdenes de magnitud más grandes que cualquier degradación del rendimiento que el isoperador te esté causando, y que todo lo que se ha oído sobre el diseño y la codificación en torno al isoperador costará una fortuna. calidad del código y, en última instancia, también será contraproducente en cuanto a rendimiento. En este caso, mantengo mi declaración. El operador 'is' nunca será el problema con el rendimiento en tiempo de ejecución.
Binary Worrier
23

Ok, estaba charlando sobre esto con alguien y decidí probarlo más. Por lo que puedo decir, el rendimiento de asy ises muy bueno, en comparación con probar su propio miembro o función para almacenar información de tipo.

Usé Stopwatch, que acabo de aprender que puede no ser el enfoque más confiable, así que también lo intenté UtcNow. Más tarde, también probé el enfoque del tiempo del procesador, que parece similar a UtcNowincluir tiempos de creación impredecibles. También intenté hacer que la clase base no fuera abstracta sin virtuales, pero no pareció tener un efecto significativo.

Ejecuté esto en un Quad Q6600 con 16 GB de RAM. Incluso con iteraciones de 50 mil, los números todavía rebotan alrededor de +/- 50 milisegundos o más, por lo que no leería demasiado en las diferencias menores.

Fue interesante ver que x64 se creaba más rápido pero se ejecutaba como / es más lento que x86

Modo de lanzamiento x64:
Cronómetro:
Como: 561ms
Es: 597ms
Propiedad base: 539ms
Campo base: 555ms
Campo RO base: 552ms Prueba
virtual GetEnumType (): 556ms Prueba
virtual IsB (): 588ms
Tiempo de creación: 10416ms

UtcNow:
As: 499ms
Is: 532ms
Propiedad base: 479ms
Campo base: 502ms
Campo RO base: 491ms
Virtual GetEnumType (): 502ms
Virtual bool IsB (): 522ms
Tiempo de creación: 285ms (Este número parece poco confiable con UtcNow. También obtengo 109ms y 806ms.)

Modo de lanzamiento x86:
Cronómetro:
Como: 391ms
Es: 423ms
Propiedad base: 369ms
Campo base: 321ms
Campo RO base: 339ms Prueba
virtual GetEnumType (): 361ms Prueba
virtual IsB (): 365ms
Tiempo de creación: 14106ms

UtcNow:
As: 348ms
Is: 375ms
Propiedad base: 329ms
Campo base: 286ms
Campo RO base: 309ms
Virtual GetEnumType (): 321ms
Virtual bool IsB (): 332ms
Tiempo de creación: 544ms (Este número parece poco confiable con UtcNow.)

Aquí está la mayor parte del código:

    static readonly int iterations = 50000000;
    void IsTest()
    {
        Process.GetCurrentProcess().ProcessorAffinity = (IntPtr)1;
        MyBaseClass[] bases = new MyBaseClass[iterations];
        bool[] results1 = new bool[iterations];

        Stopwatch createTime = new Stopwatch();
        createTime.Start();
        DateTime createStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            if (i % 2 == 0) bases[i] = new MyClassA();
            else bases[i] = new MyClassB();
        }
        DateTime createStop = DateTime.UtcNow;
        createTime.Stop();


        Stopwatch isTimer = new Stopwatch();
        isTimer.Start();
        DateTime isStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] =  bases[i] is MyClassB;
        }
        DateTime isStop = DateTime.UtcNow; 
        isTimer.Stop();
        CheckResults(ref  results1);

        Stopwatch asTimer = new Stopwatch();
        asTimer.Start();
        DateTime asStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i] as MyClassB != null;
        }
        DateTime asStop = DateTime.UtcNow; 
        asTimer.Stop();
        CheckResults(ref  results1);

        Stopwatch baseMemberTime = new Stopwatch();
        baseMemberTime.Start();
        DateTime baseStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassType == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseStop = DateTime.UtcNow;
        baseMemberTime.Stop();
        CheckResults(ref  results1);

        Stopwatch baseFieldTime = new Stopwatch();
        baseFieldTime.Start();
        DateTime baseFieldStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassTypeField == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseFieldStop = DateTime.UtcNow;
        baseFieldTime.Stop();
        CheckResults(ref  results1);


        Stopwatch baseROFieldTime = new Stopwatch();
        baseROFieldTime.Start();
        DateTime baseROFieldStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassTypeField == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseROFieldStop = DateTime.UtcNow;
        baseROFieldTime.Stop();
        CheckResults(ref  results1);

        Stopwatch virtMethTime = new Stopwatch();
        virtMethTime.Start();
        DateTime virtStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].GetClassType() == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime virtStop = DateTime.UtcNow;
        virtMethTime.Stop();
        CheckResults(ref  results1);

        Stopwatch virtMethBoolTime = new Stopwatch();
        virtMethBoolTime.Start();
        DateTime virtBoolStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].IsB();
        }
        DateTime virtBoolStop = DateTime.UtcNow;
        virtMethBoolTime.Stop();
        CheckResults(ref  results1);


        asdf.Text +=
        "Stopwatch: " + Environment.NewLine 
          +   "As:  " + asTimer.ElapsedMilliseconds + "ms" + Environment.NewLine
           +"Is:  " + isTimer.ElapsedMilliseconds + "ms" + Environment.NewLine
           + "Base property:  " + baseMemberTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Base field:  " + baseFieldTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Base RO field:  " + baseROFieldTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Virtual GetEnumType() test:  " + virtMethTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Virtual IsB() test:  " + virtMethBoolTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Create Time :  " + createTime.ElapsedMilliseconds + "ms" + Environment.NewLine + Environment.NewLine+"UtcNow: " + Environment.NewLine + "As:  " + (asStop - asStart).Milliseconds + "ms" + Environment.NewLine + "Is:  " + (isStop - isStart).Milliseconds + "ms" + Environment.NewLine + "Base property:  " + (baseStop - baseStart).Milliseconds + "ms" + Environment.NewLine + "Base field:  " + (baseFieldStop - baseFieldStart).Milliseconds + "ms" + Environment.NewLine + "Base RO field:  " + (baseROFieldStop - baseROFieldStart).Milliseconds + "ms" + Environment.NewLine + "Virtual GetEnumType():  " + (virtStop - virtStart).Milliseconds + "ms" + Environment.NewLine + "Virtual bool IsB():  " + (virtBoolStop - virtBoolStart).Milliseconds + "ms" + Environment.NewLine + "Create Time :  " + (createStop-createStart).Milliseconds + "ms" + Environment.NewLine;
    }
}

abstract class MyBaseClass
{
    public enum ClassTypeEnum { A, B }
    public ClassTypeEnum ClassType { get; protected set; }
    public ClassTypeEnum ClassTypeField;
    public readonly ClassTypeEnum ClassTypeReadonlyField;
    public abstract ClassTypeEnum GetClassType();
    public abstract bool IsB();
    protected MyBaseClass(ClassTypeEnum kind)
    {
        ClassTypeReadonlyField = kind;
    }
}

class MyClassA : MyBaseClass
{
    public override bool IsB() { return false; }
    public override ClassTypeEnum GetClassType() { return ClassTypeEnum.A; }
    public MyClassA() : base(MyBaseClass.ClassTypeEnum.A)
    {
        ClassType = MyBaseClass.ClassTypeEnum.A;
        ClassTypeField = MyBaseClass.ClassTypeEnum.A;            
    }
}
class MyClassB : MyBaseClass
{
    public override bool IsB() { return true; }
    public override ClassTypeEnum GetClassType() { return ClassTypeEnum.B; }
    public MyClassB() : base(MyBaseClass.ClassTypeEnum.B)
    {
        ClassType = MyBaseClass.ClassTypeEnum.B;
        ClassTypeField = MyBaseClass.ClassTypeEnum.B;
    }
}
Jared Thirsk
fuente
45
(Algún bono de 5 am - Shakespeare inspirado ...) Ser o no ser: esa es la pregunta: si es más noble en el código sufrir las enumeraciones y propiedades de las bases abstractas, o aceptar las ofertas de un intermediario lingüista ¿Y al invocar su instrucción, confiar en ellos? Adivinar: preguntarse; No más; y con un tiempo para discernir terminamos con el dolor de cabeza y las mil maravillas subconscientes de las que son herederos los codificadores con límite de tiempo. Es un cierre que se desea con devoción. Morir, no, pero dormir; Sí, dormiré, tal vez soñar es y como en lo que se puede derivar de la clase más baja.
Jared Thirsk
¿Podemos concluir de esto que acceder a una propiedad es más rápido en x64 que acceder a un campo? Porque eso es una gran sorpresa para mí, ¿cómo puede ser esto?
Didier A.
1
No concluiría eso, porque: "Incluso con 50 mil iteraciones, los números aún rebotan alrededor de +/- 50 milisegundos o más, por lo que no leería demasiado en las diferencias menores".
Jared Thirsk
16

Andrew tiene razón. De hecho, con el análisis de código, Visual Studio informa de esto como un reparto innecesario.

Una idea (sin saber lo que estás haciendo es un poco disparatada en la oscuridad), pero siempre me han aconsejado que evite marcar así y que en su lugar tenga otra clase. Entonces, en lugar de hacer algunas verificaciones y tener diferentes acciones según el tipo, haga que la clase sepa cómo procesarse a sí misma ...

por ejemplo, Obj puede ser ISpecialType o IType;

ambos tienen un método DoStuff () definido. Para IType, solo puede regresar o hacer cosas personalizadas, mientras que ISpecialType puede hacer otras cosas.

Esto luego elimina por completo cualquier conversión, hace que el código sea más limpio y más fácil de mantener, y la clase sabe cómo hacer sus propias tareas.

Ian
fuente
Sí, ya que todo lo que voy a hacer si las pruebas de tipo son verdaderas es llamar a un determinado método de interfaz, podría mover ese método de interfaz a la clase base y hacer que no haga nada por defecto. Eso podría ser más elegante que crear una función virtual para probar el tipo.
JubJub
Hice una prueba similar a Binary Worrier después de los comentarios de abatishchev y encontré solo 60 ms de diferencia en 10,000,000 iteraciones.
Ian
1
Vaya, gracias por la ayuda. Supongo que me limitaré a usar los operadores de verificación de tipos por ahora, a menos que parezca apropiado reorganizar la estructura de clases. Usaré el operador 'como' como sugirió Andrew, ya que no quiero emitir de forma redundante.
JubJub
15

Hice una comparación de rendimiento en dos posibilidades de comparación de tipos

  1. myobject.GetType () == typeof (MyClass)
  2. myobject es MyClass

El resultado es: ¡Usar "es" es aproximadamente 10 veces más rápido!

Salida:

Hora de comparación de tipos: 00: 00: 00.456

Hora de comparación Is: 00: 00: 00.042

Mi código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace ConsoleApplication3
{
    class MyClass
    {
        double foo = 1.23;
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyClass myobj = new MyClass();
            int n = 10000000;

            Stopwatch sw = Stopwatch.StartNew();

            for (int i = 0; i < n; i++)
            {
                bool b = myobj.GetType() == typeof(MyClass);
            }

            sw.Stop();
            Console.WriteLine("Time for Type-Comparison: " + GetElapsedString(sw));

            sw = Stopwatch.StartNew();

            for (int i = 0; i < n; i++)
            {
                bool b = myobj is MyClass;
            }

            sw.Stop();
            Console.WriteLine("Time for Is-Comparison: " + GetElapsedString(sw));
        }

        public static string GetElapsedString(Stopwatch sw)
        {
            TimeSpan ts = sw.Elapsed;
            return String.Format("{0:00}:{1:00}:{2:00}.{3:000}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds);
        }
    }
}
Knasterbax
fuente
13

El punto que Andrew Hare hizo sobre la pérdida de rendimiento cuando realizaste la isverificación y luego el lanzamiento fue válido, pero en C # 7.0 podemos hacer es verificar la coincidencia del patrón de brujas para evitar un lanzamiento adicional más adelante:

if (obj is ISpecialType st)
{
   //st is in scope here and can be used
}

Además, si necesita verificar entre varios tipos, las construcciones de coincidencia de patrones de C # 7.0 ahora le permiten hacer switchen tipos:

public static double ComputeAreaModernSwitch(object shape)
{
    switch (shape)
    {
        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Rectangle r:
            return r.Height * r.Length;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

Puede leer más sobre la coincidencia de patrones en C # en la documentación aquí .

Krzysztof Branicki
fuente
1
Una solución válida, seguro, pero esta característica de coincidencia de patrones de C # me entristece, cuando fomenta el código de "envidia de características" como este. Seguramente deberíamos esforzarnos por encapsular la lógica donde sólo los objetos derivados "saben" cómo calcular su propia área, y luego simplemente devuelven el valor.
Dib
2
SO necesita botones de filtro (en la pregunta) para las respuestas que se aplican a las versiones más recientes de un marco, plataforma, etc. Esta respuesta forma la base de la correcta para C # 7.
Nick Westgate
1
Los ideales de @Dib OOP se eliminan por la ventana cuando trabaja con tipos / clases / interfaces que no controla. Este enfoque también es útil cuando se maneja el resultado de una función que puede devolver uno de muchos valores de tipos completamente diferentes (debido a que C # todavía no admite tipos de unión todavía; puede usar bibliotecas como, OneOf<T...>pero tienen deficiencias importantes) .
Dai
4

En caso de que alguien se lo pregunte, hice pruebas en el motor Unity 2017.1, con scripting runtime versión .NET4.6 (Experimantal) en un portátil con CPU i5-4200U. Resultados:

Average Relative To Local Call LocalCall 117.33 1.00 is 241.67 2.06 Enum 139.33 1.19 VCall 294.33 2.51 GetType 276.00 2.35

Artículo completo: http://www.ennoble-studios.com/tuts/unity-c-performance-comparison-is-vs-enum-vs-virtual-call.html

Gru
fuente
El enlace del artículo está muerto.
James Wilkins
El enlace de @James revivió.
Gru
Buenas cosas, pero no te voté negativamente (en realidad, voté a favor de todos modos); En caso de que se lo esté preguntando. :)
James Wilkins
-3

Siempre me han aconsejado que evite marcar de esta manera y que en su lugar tenga otra clase. Entonces, en lugar de hacer algunas comprobaciones y tener diferentes acciones según el tipo, haga que la clase sepa cómo procesarse a sí misma ...

por ejemplo, Obj puede ser ISpecialType o IType;

ambos tienen un método DoStuff () definido. Para IType, solo puede regresar o hacer cosas personalizadas, mientras que ISpecialType puede hacer otras cosas.

Esto luego elimina por completo cualquier conversión, hace que el código sea más limpio y más fácil de mantener, y la clase sabe cómo hacer sus propias tareas.

usuario3802787
fuente
1
Esto no responde a la pregunta. De todos modos, es posible que las clases no siempre sepan cómo procesarse a sí mismas debido a la falta de contexto. Aplicamos una lógica similar al manejo de excepciones cuando permitimos que las excepciones suban en la cadena de llamadas hasta que algún método / función tenga suficiente contexto para manejar los errores.
Vakhtang