C # "è" prestazioni dell'operatore


102

Ho un programma che richiede prestazioni veloci. All'interno di uno dei suoi cicli interni, ho bisogno di testare il tipo di un oggetto per vedere se eredita da una certa interfaccia.

Un modo per farlo sarebbe con la funzionalità di controllo del tipo incorporata in CLR. Il metodo più elegante è probabilmente la parola chiave "is":

if (obj is ISpecialType)

Un altro approccio sarebbe quello di dare alla classe base la mia funzione virtuale GetType () che restituisce un valore enum predefinito (nel mio caso, in realtà, ho solo bisogno di un bool). Quel metodo sarebbe veloce, ma meno elegante.

Ho sentito che esiste un'istruzione IL specifica per la parola chiave "is", ma ciò non significa che venga eseguita velocemente quando tradotta in assembly nativo. Qualcuno può condividere alcune informazioni sull'esecuzione di "è" rispetto all'altro metodo?

AGGIORNAMENTO: Grazie per tutte le risposte informate! Sembra che un paio di punti utili siano sparsi tra le risposte: il punto di Andrew su "sta" eseguendo automaticamente un cast è essenziale, ma anche i dati sulle prestazioni raccolti da Binary Worrier e Ian sono estremamente utili. Sarebbe fantastico se una delle risposte venisse modificata per includere tutte queste informazioni.


2
btw, CLR non ti darà la possibilità di creare la tua funzione Type GetType (), perché infrange una delle regole CLR principali - veramente tipi
abatishchev

1
Ehm, non sono completamente sicuro di cosa intendi con la regola "veramente tipi", ma capisco che il CLR ha una funzione incorporata Type GetType (). Se dovessi usare quel metodo, sarebbe con una funzione di un nome diverso che restituisce un enum, quindi non ci sarebbe alcun conflitto nome / simbolo.
JubJub

3
Penso che abatishchev significasse "protezione dai tipi". GetType () non è virtuale per impedire a un tipo di mentire su se stesso e quindi preservare l'indipendenza dai tipi.
Andrew Hare

2
Hai preso in considerazione il prelievo e la memorizzazione nella cache della conformità del tipo in modo da non doverlo fare all'interno di cicli? Sembra che ogni domanda di perf sia sempre fatta massicciamente di +1, ma questa mi sembra solo una scarsa comprensione di c #. In realtà è troppo lento? Come? Cosa hai provato? Ovviamente non molto visti i tuoi commenti sulle risposte ...
Gusdor

Risposte:


114

L'utilizzo ispuò danneggiare le prestazioni se, una volta controllato il tipo, si esegue il cast su quel tipo. isesegue effettivamente il cast dell'oggetto al tipo che si sta verificando, quindi qualsiasi casting successivo è ridondante.

Se intendi trasmettere comunque, ecco un approccio migliore:

ISpecialType t = obj as ISpecialType;

if (t != null)
{
    // use t here
}

1
Grazie. Ma se non ho intenzione di eseguire il cast dell'oggetto se il condizionale fallisce, sarebbe meglio usare una funzione virtuale per testare il tipo invece?
JubJub

4
@JubJub: no. Un errore asfondamentalmente esegue la stessa operazione di is(ovvero, il controllo del tipo). L'unica differenza è che poi ritorna nullinvece di false.
Konrad Rudolph,

74

Sono con Ian , probabilmente non vuoi farlo.

Tuttavia, solo per quello che sai, c'è pochissima differenza tra i due, oltre 10.000.000 di iterazioni

  • Il controllo dell'enumerazione arriva a 700 millisecondi (circa)
  • Il controllo IS arriva a 1000 millisecondi (circa)

Personalmente non risolverei questo problema in questo modo, ma se fossi costretto a scegliere un metodo sarebbe il controllo IS integrato, la differenza di prestazioni non vale la pena considerare l'overhead di codifica.

Le mie classi base e derivate

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: Come richiesto maggiori info sui test.

Ho eseguito entrambi i test da un'app console (una build di debug), ogni test ha il seguente aspetto

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

Eseguendo in versione, ottengo una differenza di 60 - 70 ms, come Ian.

Ulteriore aggiornamento - 25 ottobre 2012
Dopo un paio d'anni di distanza ho notato qualcosa al riguardo, il compilatore può scegliere di omettere bool b = a is MyClassBnel rilascio perché b non viene utilizzato da nessuna parte.

Questo codice. . .

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 costantemente il iscontrollo che arriva a circa 57 millisecondi e il confronto enum che arriva a 29 millisecondi.

NB Preferirei comunque il iscontrollo, la differenza è troppo piccola per preoccuparsene


35
+1 per testare effettivamente le prestazioni, invece di presumere.
Jon Tackabury

3
È molto meglio fare un test con la classe Stopwatch, invece di DateTime
Ora

2
Lo prenderò in considerazione, tuttavia in questo caso non credo che influenzerebbe il risultato. Grazie :)
Binary Worrier

11
@Binary Worrier- Le tue nuove assegnazioni di classi da parte dell'operatore oscureranno completamente le differenze di prestazioni nelle operazioni "is". Perché non rimuovi queste nuove operazioni, riutilizzando due diverse istanze pre-allocate, quindi riesegui il codice e pubblica i risultati.

1
@mcmillab: garantirò che qualunque cosa tu stia facendo, avrai colli di bottiglia molti ordini di grandezza maggiori di qualsiasi degrado delle prestazioni che l' isoperatore ti sta causando, e che il troppo sentito parlare di progettazione e codifica attorno isall'operatore costerà una fortuna qualità del codice e alla fine sarà anche autodistruttivo per quanto riguarda le prestazioni. In questo caso, resto fedele alla mia dichiarazione. L'operatore "is" non sarà mai il problema delle prestazioni di runtime.
Binary Worrier

23

Ok, stavo parlando di questo con qualcuno e ho deciso di testarlo di più. Per quanto ne so, le prestazioni di ase issono entrambe molto buone, rispetto al test del proprio membro o funzione per memorizzare le informazioni sul tipo.

Ho usato Stopwatch, che ho appena imparato potrebbe non essere l'approccio più affidabile, quindi ho anche provato UtcNow. Successivamente, ho anche provato l'approccio del tempo del processore che sembra simile a UtcNowincludere tempi di creazione imprevedibili. Ho anche provato a rendere la classe base non astratta senza virtuali ma non sembrava avere un effetto significativo.

L'ho eseguito su un Quad Q6600 con 16 GB di RAM. Anche con 50mil iterazioni, i numeri rimbalzano ancora intorno a +/- 50 o giù di lì millisec, quindi non leggerei troppo le differenze minori.

È stato interessante vedere che x64 è stato creato più velocemente ma eseguito poiché / è più lento di x86

x64 Modalità di rilascio:
Cronometro:
As: 561 ms
Is: 597 ms
Proprietà base: 539 ms
Campo base: 555 ms
Campo RO base: 552
ms Test Virtual GetEnumType (): 556
ms Test Virtual IsB (): 588 ms
Ora creazione: 10416 ms

UtcNow:
As: 499 ms
Is: 532 ms
Proprietà base: 479 ms
Campo base: 502 ms
Campo RO base: 491 ms
Virtual GetEnumType (): 502 ms Bool
virtuale IsB (): 522 ms
Ora creazione: 285 ms (Questo numero sembra inaffidabile con UtcNow. Ricevo anche 109 ms e 806 ms.)

Modalità di rilascio x86:
Cronometro:
As: 391 ms
Is: 423 ms
Proprietà base: 369 ms
Campo base: 321 ms
Campo RO base: 339
ms Test Virtual GetEnumType (): 361
ms Test IsB virtuale (): 365 ms
Ora creazione: 14106 ms

UtcNow:
As: 348 ms
Is: 375 ms
Proprietà base: 329 ms
Campo base: 286 ms
Campo RO base: 309 ms
Virtual GetEnumType (): 321 ms Bool
virtuale IsB (): 332 ms
Ora creazione: 544 ms (Questo numero sembra inaffidabile con UtcNow.)

Ecco la maggior parte del codice:

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

45
(Alcuni bonus 5 ispirano Shakespeare ...) Essere o non essere: questa è la domanda: se è più nobile nel codice soffrire le enumerazioni e le proprietà delle basi astratte, o accettare le offerte di un intermediario linguista E invocando le sue istruzioni, fidarsi di loro? Indovinare: meravigliarsi; Non piu; e con un tempismo per discernere poniamo fine al mal di testa e alle mille meraviglie inconsce di cui sono eredi i programmatori legati al tempo. È una chiusura da desiderare devotamente. Morire, no, ma dormire; Sì, dormirò, forse sognare è e come può essere derivato dalla più bassa classe.
Jared Thirsk

Possiamo concludere da ciò che l'accesso a una proprietà è più veloce su x64 rispetto all'accesso a un campo !!! Perché è una bella sorpresa per me, come può essere?
Didier A.

1
Non lo concluderei, perché: "Anche con 50mil iterazioni, i numeri continuano a rimbalzare intorno a +/- 50 o giù di lì millisec, quindi non leggerei troppo le differenze minori".
Jared Thirsk

16

Andrew ha ragione. In effetti, con l'analisi del codice questo viene segnalato da Visual Studio come un cast non necessario.

Un'idea (senza sapere cosa stai facendo è un po 'una ripresa nel buio), ma mi è sempre stato consigliato di evitare di fare controlli in questo modo, e invece ho un'altra lezione. Quindi, piuttosto che fare alcuni controlli e avere azioni diverse a seconda del tipo, fai in modo che la classe sappia come elaborare se stessa ...

ad esempio, Obj può essere ISpecialType o IType;

entrambi hanno un metodo DoStuff () definito. Per IType può semplicemente restituire o fare cose personalizzate, mentre ISpecialType può fare altre cose.

Questo quindi rimuove completamente qualsiasi casting, rende il codice più pulito e più facile da mantenere e la classe sa come svolgere le proprie attività.


Sì, poiché tutto ciò che farò se il tipo risulta vero è chiamare un certo metodo di interfaccia su di esso, potrei semplicemente spostare quel metodo di interfaccia nella classe base e non fare nulla per impostazione predefinita. Potrebbe essere più elegante della creazione di una funzione virtuale per testare il tipo.
JubJub

Ho fatto un test simile a Binary Worrier dopo i commenti di abatishchev e ho trovato solo 60 ms di differenza su 10.000.000 di itterazioni.
Ian

1
Wow, grazie per l'aiuto. Suppongo che per ora continuerò a usare gli operatori di controllo del tipo, a meno che non sembri opportuno riorganizzare la struttura della classe. Userò l'operatore "as" come suggerito da Andrew poiché non voglio eseguire il cast in modo ridondante.
JubJub

15

Ho fatto un confronto delle prestazioni su due possibilità di confronto del tipo

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

Il risultato è: l'utilizzo di "è" è circa 10 volte più veloce !!!

Produzione:

Tempo per il confronto del tipo: 00: 00: 00.456

Tempo per il confronto: 00: 00: 00.042

Il mio codice:

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

13

Il punto che Andrew Hare ha fatto riguardo alle prestazioni perse quando si esegue il iscontrollo e poi il cast era valido, ma in C # 7.0 possiamo fare è controllare la corrispondenza del pattern strega per evitare lanci aggiuntivi in ​​seguito:

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

Inoltre, se è necessario controllare tra più tipi, i costrutti di pattern matching di C # 7.0 ora consentono di eseguire operazioni switchsui tipi:

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

Puoi leggere ulteriori informazioni sulla corrispondenza dei modelli in C # nella documentazione qui .


1
Una soluzione valida, di sicuro, ma questa funzionalità di corrispondenza dei modelli in C # mi rattrista quando incoraggia il codice "invidia delle funzionalità" come questo. Sicuramente dovremmo lottare per l'incapsulamento della logica in cui solo gli oggetti derivati ​​"sanno" come calcolare la propria area e quindi restituiscono semplicemente il valore?
Dib

2
SO ha bisogno di pulsanti di filtro (sulla domanda) per le risposte che si applicano alle versioni più recenti di un framework, piattaforma ecc. Questa risposta costituisce la base di quella corretta per C # 7.
Nick Westgate

1
Gli ideali di @Dib OOP vengono buttati fuori dalla finestra quando lavori con tipi / classi / interfacce che non controlli. Questo approccio è utile anche per quando si gestisce il risultato di una funzione che può restituire uno dei tanti valori di tipi completamente diversi (perché C # ancora non supporta ancora i tipi di unione: è possibile utilizzare librerie simili OneOf<T...>ma presentano gravi carenze) .
Dai

4

Nel caso qualcuno se lo stesse chiedendo, ho fatto dei test nel motore Unity 2017.1, con versione runtime di scripting .NET4.6 (Experimantal) su un notebook con CPU i5-4200U. Risultati:

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

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


Il collegamento dell'articolo è morto.
James Wilkins

@James link rianimato.
Gru

Roba buona - ma non ti ho votato negativamente (in realtà ho votato comunque); Nel caso ve lo stiate chiedendo. :)
James Wilkins

-3

Mi è sempre stato consigliato di evitare di fare controlli in questo modo e di tenere invece un'altra lezione. Quindi, piuttosto che fare alcuni controlli e avere azioni diverse a seconda del tipo, fai in modo che la classe sappia come elaborare se stessa ...

ad esempio, Obj può essere ISpecialType o IType;

entrambi hanno un metodo DoStuff () definito. Per IType può semplicemente restituire o fare cose personalizzate, mentre ISpecialType può fare altre cose.

Questo quindi rimuove completamente qualsiasi casting, rende il codice più pulito e più facile da mantenere e la classe sa come svolgere le proprie attività.


1
Questo non risponde alla domanda. Ad ogni modo, le classi potrebbero non sempre sapere come elaborare se stesse a causa della mancanza di contesto. Applichiamo una logica simile alla gestione delle eccezioni quando permettiamo alle eccezioni di risalire la catena di chiamate fino a quando un metodo / funzione non ha abbastanza contesto per gestire gli errori.
Vakhtang
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.