C # 'é' desempenho do operador

102

Tenho um programa que exige desempenho rápido. Em um de seus loops internos, preciso testar o tipo de um objeto para ver se ele herda de uma determinada interface.

Uma maneira de fazer isso seria com a funcionalidade de verificação de tipo integrada do CLR. O método mais elegante provavelmente sendo a palavra-chave 'é':

if (obj is ISpecialType)

Outra abordagem seria dar à classe base minha própria função virtual GetType () que retorna um valor de enum predefinido (no meu caso, na verdade, eu só preciso de um bool). Esse método seria rápido, mas menos elegante.

Ouvi dizer que existe uma instrução IL especificamente para a palavra-chave 'é', mas isso não significa que ela executa rapidamente quando traduzida para o assembly nativo. Alguém pode compartilhar alguns insights sobre o desempenho de 'é' em relação ao outro método?

ATUALIZAÇÃO: Obrigado por todas as respostas informadas! Parece que alguns pontos úteis estão espalhados entre as respostas: O ponto de Andrew sobre 'é' executar um elenco automaticamente é essencial, mas os dados de desempenho coletados por Binary Worrier e Ian também são extremamente úteis. Seria ótimo se uma das respostas fosse editada para incluir todas essas informações.

JubJub
fonte
2
btw, CLR não lhe dará a possibilidade de criar sua própria função Type GetType (), porque quebra uma das principais regras CLR - verdadeiramente tipos
abatishchev
1
Er, não tenho certeza do que você quer dizer com a regra "verdadeiramente tipos", mas entendo que o CLR tem uma função Type GetType () embutida. Se eu fosse usar esse método, seria com uma função de um nome diferente retornando algum enum, então não haveria nenhum conflito de nome / símbolo.
JubJub
3
Acho que abatishchev significava "segurança de tipo". GetType () não é virtual para evitar que um tipo mentir sobre si mesmo e, portanto, preservar a segurança do tipo.
Andrew Hare
2
Você considerou a pré-busca e o armazenamento em cache da conformidade de tipo para que não precise fazer isso dentro de loops? Parece que todas as perguntas de desempenho são sempre marcadas com +1, mas isso me parece um entendimento insatisfatório de c #. É realmente muito lento? Quão? O que você tentou? Obviamente não muito, dados seus comentários sobre as respostas ...
Gusdor

Respostas:

114

O uso ispode prejudicar o desempenho se, depois de verificar o tipo, você lançar para aquele tipo. isna verdade, converte o objeto para o tipo que você está verificando, portanto, qualquer conversão subsequente é redundante.

Se você vai lançar de qualquer maneira, aqui está uma abordagem melhor:

ISpecialType t = obj as ISpecialType;

if (t != null)
{
    // use t here
}
Andrew Hare
fonte
1
Obrigado. Mas se não vou lançar o objeto se a condicional falhar, seria melhor usar uma função virtual para testar o tipo?
JubJub
4
@JubJub: não. Uma falha asexecuta basicamente a mesma operação que is(ou seja, a verificação de tipo). A única diferença é que ele retorna em nullvez de false.
Konrad Rudolph
74

Estou com Ian , você provavelmente não quer fazer isso.

No entanto, só para você saber, há muito pouca diferença entre os dois, mais de 10.000.000 de iterações

  • A verificação enum chega em 700 milissegundos (aprox)
  • A verificação IS chega em 1000 milissegundos (aprox)

Eu pessoalmente não resolveria esse problema dessa maneira, mas se fosse forçado a escolher um método seria a verificação de IS embutida, a diferença de desempenho não vale a pena considerar a sobrecarga de codificação.

Minhas classes base e 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: Conforme solicitado, mais informações sobre os testes.

Eu executei os dois testes de um aplicativo de console (uma compilação de depuração), cada teste se parece com o seguinte

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

Correndo na liberação, obtenho uma diferença de 60 - 70 ms, como Ian.

Atualização adicional - 25 de outubro de 2012
Depois de alguns anos longe, percebi algo sobre isso, o compilador pode escolher omitir bool b = a is MyClassBno lançamento porque b não é usado em lugar nenhum.

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

. . . mostra consistentemente ois verificação chegando em aproximadamente 57 milissegundos e a comparação de enum chegando em 29 milissegundos.

NB eu ainda prefiro o ischeque, a diferença é muito pequena para me preocupar

Preocupante binário
fonte
35
+1 para realmente testar o desempenho, em vez de assumir.
Jon Tackabury
3
É muito melhor fazer o teste com a classe Cronômetro, em vez de DateTime.Agora, que é muito caro
abatishchev
2
Vou levar isso em consideração, porém, neste caso, não acho que isso afetaria o resultado. Obrigado :)
Binary Worrier
11
@Binary Worrier- Suas novas alocações de classes de operadores irão ofuscar completamente quaisquer diferenças de desempenho nas operações 'is'. Por que você não remove essas novas operações, reutilizando duas instâncias pré-alocadas diferentes e, em seguida, executa novamente o código e publica seus resultados.
1
@mcmillab: Garanto que, independentemente do que esteja fazendo, você terá gargalos muitas ordens de magnitude maiores do que qualquer degradação de desempenho que a isoperadora esteja causando, e que ouvir falar de projetar e codificar em torno da isoperadora custará uma fortuna em qualidade de código e, no final das contas, terá desempenho autodestrutivo também. Neste caso, mantenho minha declaração. O operador 'é' nunca será o problema com o desempenho do seu tempo de execução.
Binary Worrier
23

Ok, então eu estava conversando sobre isso com alguém e decidi testar mais. Pelo que eu posso dizer, o desempenho de ase isé muito bom, em comparação com o teste de seu próprio membro ou função para armazenar informações de tipo.

Eu usei Stopwatch, que acabei de descobrir que pode não ser a abordagem mais confiável, então também tentei UtcNow. Mais tarde, também tentei a abordagem de tempo do processador, que parece semelhante a UtcNowincluir tempos de criação imprevisíveis. Eu também tentei tornar a classe base não abstrata sem virtuais, mas não pareceu ter um efeito significativo.

Eu executei isso em um Quad Q6600 com 16 GB de RAM. Mesmo com iterações de 50mil, os números ainda oscilam em torno de +/- 50 ou mais milissegundos, então eu não interpretaria muito as pequenas diferenças.

Foi interessante ver que o x64 foi criado mais rápido, mas executado como / é mais lento que o x86

x64 Modo de liberação:
Cronômetro:
As: 561ms
Is: 597ms
Propriedade básica: 539ms
Campo base: 555ms
Campo RO base: 552ms
Teste GetEnumType ()
virtual : 556ms Teste IsB virtual (): 588ms
Tempo de criação: 10416ms

UtcNow:
As: 499ms
Is: 532ms
Propriedade de base: 479ms
Campo de base: 502ms
Campo de RO base: 491ms
Virtual GetEnumType (): 502ms
Virtual bool IsB (): 522ms
Tempo de criação: 285ms (Este número parece não confiável com UtcNow. Eu também recebo 109ms e 806ms.)

x86 Modo de liberação:
Cronômetro:
As: 391ms
Is: 423ms
Propriedade básica: 369ms
Campo base: 321ms
Campo RO de base: 339ms
Teste GetEnumType ()
virtual : 361ms Teste IsB virtual (): 365ms
Tempo de criação: 14106ms

UtcNow:
As: 348ms
Is: 375ms
Propriedade de base: 329ms
Campo de base: 286ms
Campo de RO base: 309ms
Virtual GetEnumType (): 321ms
Virtual bool IsB (): 332ms
Tempo de Criação: 544ms (Este número parece não confiável com UtcNow.)

Aqui está a maior parte do 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
fonte
45
(Alguns bônus inspiraram Shakespeare ...) Ser, ou não ser: eis a questão: Se é mais nobre no código sofrer As enumerações e propriedades de bases abstratas, Ou aceitar as ofertas de um intermediário lingüista E ao invocar sua instrução, confia neles? Adivinhar: imaginar; Não mais; e por um tempo para discernir, acabamos com a dor de cabeça e as mil maravilhas subconscientes das quais os codificadores vinculados ao tempo são herdeiros. É um encerramento a ser desejado com devoção. Morrer, não, mas dormir; Sim, vou dormir, porventura sonhar é e como no que pode ser derivado da mais base de classe.
Jared Thirsk
Podemos concluir que acessar uma propriedade é mais rápido no x64 do que acessar um campo !!! Porque é uma grande surpresa para mim como isso pode ser?
Didier A.
1
Eu não concluiria isso, porque: "Mesmo com iterações de 50mil, os números ainda oscilam em torno de +/- 50 ou mais milissegundos, então eu não interpretaria muito as pequenas diferenças."
Jared Thirsk
16

Andrew está correto. Na verdade, com a análise de código, isso é relatado pelo Visual Studio como um elenco desnecessário.

Uma ideia (sem saber o que você está fazendo é meio que um tiro no escuro), mas sempre fui aconselhado a evitar checar assim e, em vez disso, ter outra aula. Então, ao invés de fazer algumas verificações e ter ações diferentes dependendo do tipo, faça a classe saber como se processar ...

por exemplo, Obj pode ser ISpecialType ou IType;

ambos têm um método DoStuff () definido. Para IType, ele pode apenas retornar ou fazer coisas personalizadas, enquanto ISpecialType pode fazer outras coisas.

Isso remove completamente qualquer projeção, torna o código mais limpo e mais fácil de manter, e a classe sabe como fazer suas próprias tarefas.

Ian
fonte
Sim, já que tudo o que farei se o tipo de teste for verdadeiro for chamar um determinado método de interface nele, eu poderia simplesmente mover esse método de interface para a classe base e fazer com que ele não fizesse nada por padrão. Isso pode ser mais elegante do que criar uma função virtual para testar o tipo.
JubJub
Fiz um teste semelhante ao Binary Worrier após os comentários de abatishchev e encontrei apenas 60 ms de diferença em 10.000.000 itterações.
Ian
1
Nossa, obrigado pela ajuda. Suponho que vou me limitar a usar os operadores de verificação de tipo por enquanto, a menos que pareça apropriado reorganizar a estrutura da classe. Usarei o operador 'as' como Andrew sugeriu, já que não quero lançar redundantemente.
JubJub
15

Eu fiz uma comparação de desempenho em duas possibilidades de comparação de tipo

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

O resultado é: Usar "é" é cerca de 10 vezes mais rápido !!!

Resultado:

Hora para comparação de tipo: 00: 00: 00.456

Hora da comparação de is: 00: 00: 00.042

Meu 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
fonte
13

O ponto que Andrew Hare fez sobre a perda de desempenho quando você executa a isverificação e, em seguida, o elenco era válido, mas no C # 7.0 podemos fazer é verificar a correspondência de padrão de bruxa para evitar elenco adicional mais tarde:

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

Além disso, se você precisar verificar entre vários tipos, as construções de correspondência de padrões C # 7.0 agora permitem que você faça switchnos 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));
    }
}

Você pode ler mais sobre correspondência de padrões em C # na documentação aqui .

Krzysztof Branicki
fonte
1
Uma solução válida, com certeza, mas esse recurso de correspondência de padrões C # me deixa triste, quando incentiva códigos de "inveja de recursos" como este. Certamente deveríamos nos esforçar para encapsular a lógica onde apenas os objetos derivados "sabem" como calcular sua própria área, e então eles apenas retornam o valor?
Dib
2
SO precisa de botões de filtro (na pergunta) para respostas que se aplicam a versões mais recentes de uma estrutura, plataforma, etc. Esta resposta forma a base da resposta correta para C # 7.
Nick Westgate
1
Os ideais do @Dib OOP são jogados fora da janela quando você está trabalhando com tipos / classes / interfaces que você não controla. Essa abordagem também é útil para lidar com o resultado de uma função que pode retornar um de muitos valores de tipos completamente diferentes (porque C # ainda não oferece suporte a tipos de união - você pode usar bibliotecas como, OneOf<T...>mas elas têm grandes deficiências) .
Dia
4

Caso alguém esteja se perguntando, fiz testes no motor Unity 2017.1, com scripting runtime versão .NET4.6 (Experimantal) em um notebook com 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

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

Gru
fonte
O link do artigo está morto.
James Wilkins
Link de @James revivido.
Gru
Coisas boas - mas eu não votei contra você (na verdade, votei contra você de qualquer maneira); Caso você esteja se perguntando. :)
James Wilkins
-3

Sempre fui aconselhado a evitar checar assim e, em vez disso, ter outra aula. Então, ao invés de fazer algumas verificações e ter ações diferentes dependendo do tipo, faça a classe saber como se processar ...

por exemplo, Obj pode ser ISpecialType ou IType;

ambos têm um método DoStuff () definido. Para IType, ele pode apenas retornar ou fazer coisas personalizadas, enquanto ISpecialType pode fazer outras coisas.

Isso remove completamente qualquer projeção, torna o código mais limpo e mais fácil de manter, e a classe sabe como fazer suas próprias tarefas.

user3802787
fonte
1
Isso não responde à pergunta. De qualquer forma, as classes nem sempre sabem como se processar por falta de contexto. Aplicamos uma lógica semelhante ao tratamento de exceções quando permitimos que as exceções subam na cadeia de chamadas até que algum método / função tenha contexto suficiente para tratar os erros.
Vakhtang