C# 'is' operator performance
Asked Answered
F

8

123

I have a program that requires fast performance. Within one of its inner loops, I need to test the type of an object to see whether it inherits from a certain interface.

One way to do this would be with the CLR's built-in type-checking functionality. The most elegant method there probably being the 'is' keyword:

if (obj is ISpecialType)

Another approach would be to give the base class my own virtual GetType() function which returns a pre-defined enum value (in my case, actually, i only need a bool). That method would be fast, but less elegant.

I have heard that there is an IL instruction specifically for the 'is' keyword, but that doesn't mean it executes fast when translated into native assembly. Can anyone share some insight into the performance of 'is' versus the other method?

UPDATE: Thanks for all the informed answers! It seem a couple helpful points are spread out among the answers: Andrew's point about 'is' automatically performing a cast is essential, but the performance data gathered by Binary Worrier and Ian is also extremely useful. It would be great if one of the answers were edited to include all of this information.

Fescennine answered 26/3, 2009 at 16:4 Comment(4)
btw, CLR will not give you a possibility to create your own Type GetType() function, because it breaks one of main CLR rules - truly typesMaharajah
Er, I'm not completely sure what you mean by the "truly types" rule, but I understand that the CLR has a built-in Type GetType() function. If I were to use that method, it would be with a function of a different name returning some enum, so there wouldn't be any name/symbol conflict.Fescennine
I think abatishchev meant "type safety". GetType() is non-virtual to prevent a type from lying about itself and therefore preserving type safety.Maugre
Have you considered pre-fetching and caching the type compliance so that you dont have to do it within loops? Seems every perf question is always massively +1'd but this just seems like poor understanding of c# to me. Is it actually too slow? How? What have you tried? Obviously not much given your comments on the answers...Kinna
M
118

Using is can hurt performance if, once you check the type, you cast to that type. is actually casts the object to the type you are checking so any subsequent casting is redundant.

If you are going to cast anyway, here is a better approach:

ISpecialType t = obj as ISpecialType;

if (t != null)
{
    // use t here
}
Maugre answered 26/3, 2009 at 16:9 Comment(5)
Thanks. But if I'm not going to cast the object if the conditional fails, would I be better off using a virtual function to test the type instead?Fescennine
@JubJub: no. A failing as basically performs the same operation as is (namely, the type check). The only difference is that it then returns null instead of false.Moth
I just want to point out that, in the last few years, we gained the ability to use the following pattern: if (obj is ISpecialType t) { t.DoThing(); }Showmanship
Stackoverflow should delete outdated answers, which will be misleading for future generations.Hyperaemia
@Hyperaemia what's outdated about this answer?Strangulation
C
84

I'm with Ian, you probably don't want to do this.

However, just so you know, there is very little difference between the two, over 10,000,000 iterations

  • The enum check comes in at 700 milliseconds (approx)
  • The IS check comes in at 1000 milliseconds (approx)

I personally wouldn't fix this problem this way, but if I was forced to pick one method it would be the built in IS check, the performance difference isn't worth considering the coding overhead.

My base and derived classes

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: As requested more info on the tests.

I ran both tests from a console app (a debug build) each test looks like the following

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

Running in release, I get a difference of 60 - 70 ms, like Ian.

Further Update - Oct 25th 2012
After a couple of years away I noticed something about this, the compiler can choose to omit bool b = a is MyClassB in release because b isn't used anywhere.

This code . . .

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

. . . consistently shows the is check coming in at approx 57 milliseconds, and the enum comparison coming in at 29 milliseconds.

NB I'd still prefer the is check, the difference is too small to care about

Ceric answered 26/3, 2009 at 16:37 Comment(14)
+1 for actually testing the performance, instead of assuming.Ramunni
Thanks for testing! Ian commented above that he conducted a performance test and the difference between methods was even slighter than what you found. I'm curious how you came to your numbers.Fescennine
Thanks for the update! If I understand correctly, your second test invalidates the earlier results of 700 vs 1000 ms. Perhaps you should update the post to reflect this and incorporate Andrew's answer as well to make this the most informative answer.Fescennine
It's much better to do test with Stopwatch class, instead of DateTime.Now which is very expensiveMaharajah
I'll take that on board, however in this instance I don't think it would affect the outcome. Thanks :)Ceric
This is off-topic, but you may want to read this article explaining the pros and cons of DateTime.Now vs Stopwatch vs other timing solutions. kristofverbiest.blogspot.com/2008/10/beware-of-stopwatch.htmlCockshy
@Binary Worrier- Your new operator allocations of classes are going to completely overshadow any performance differences in the 'is' operations. Why don't you remove those new operations, by reusing two different pre-allocated instances, and then re-run the code and post your results.Owenism
comments like "the difference is too small to care about" always concern me - you don't know how important it is to the specific circumstanceLucilla
@mcmillab: I will guarantee that whatever you're doing, you'll have bottlenecks many orders of magnitude larger than any performance degradation the is operator is causing you, and that the over heard of designing & coding around the is operator will cost a fortune in code quality and will ultimately be self defeating performance wise also. In this instance I stand by my statement. The 'is' operator is never going to be the problem with your runtime performance.Ceric
@BinaryWorrier: I understand the sentiment, but I can't agree. You don't know how often the is operator is being called, and you don't know how important speed may be. There may well be other areas that can be optimized, but in some circumstances every bit of speed matters.Lucilla
@mcmillab: And again I disagree, if you find the is operator is your problem, then you've made some major mistakes elsewhere, and "fixing" the is operator isn't actually going to fix that other problem. I think we may agree to disagree on this one.Ceric
@BinaryWorrier - agreed, let's just disagree :)Lucilla
@BinaryWorrier [half a generation or so later:] You are probably right about the allocations dominating the performance. But using the same object over and over again opens the door to optimizing everything away because the compiler can trivially prove that the reference never changes, so a single check is enough and all subsequent ones must have the same result. The common dangers of benchmarks...Archuleta
@Peter-ReinstateMonica: Good points, however, over 10 million runs one method is always slower than the other, which is why I finish with the statement I'd still prefer the is check, the difference is too small to care aboutCeric
T
25

Ok so I was chatting about this with someone and decided to test this more. As far as I can tell, the performance of as and is are both very good, compared to testing your own member or function to store type information.

I used Stopwatch, which I just learned may not be the most reliable approach, so I also tried UtcNow. Later, I also tried Processor time approach which seems similar to UtcNow including unpredictable create times. I also tried making the base class non-abstract with no virtuals but it didn't seem to have a significant effect.

I ran this on a Quad Q6600 with 16GB RAM. Even with 50mil iterations, the numbers still bounce around +/- 50 or so millisec so I wouldn't read too much into the minor differences.

It was interesting to see that x64 created faster but executed as/is slower than x86

x64 Release Mode:
Stopwatch:
As: 561ms
Is: 597ms
Base property: 539ms
Base field: 555ms
Base RO field: 552ms
Virtual GetEnumType() test: 556ms
Virtual IsB() test: 588ms
Create Time : 10416ms

UtcNow:
As: 499ms
Is: 532ms
Base property: 479ms
Base field: 502ms
Base RO field: 491ms
Virtual GetEnumType(): 502ms
Virtual bool IsB(): 522ms
Create Time : 285ms (This number seems unreliable with UtcNow. I also get 109ms and 806ms.)

x86 Release Mode:
Stopwatch:
As: 391ms
Is: 423ms
Base property: 369ms
Base field: 321ms
Base RO field: 339ms
Virtual GetEnumType() test: 361ms
Virtual IsB() test: 365ms
Create Time : 14106ms

UtcNow:
As: 348ms
Is: 375ms
Base property: 329ms
Base field: 286ms
Base RO field: 309ms
Virtual GetEnumType(): 321ms
Virtual bool IsB(): 332ms
Create Time : 544ms (This number seems unreliable with UtcNow.)

Here's most of the code:

    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;
    }
}
Trainee answered 29/11, 2009 at 23:2 Comment(3)
(Some bonus 5am-inspired Shakespeare...) To be, or not to be: that is the question: Whether 'tis nobler in the code to suffer The enumerations and properties of abstract bases, Or to take up the offers of an intermediary linguist And by invoking its instruction, trust them? To guess: to wonder; No more; and by a timing to discern we end the headache and the thousand subconscious wonderings that time-bound coders are heir to. 'Tis a closure Devoutly to be wish'd. To die, no, but to sleep; Yes I shall sleep, perchance to dream of is and as in what may be derived from the most base of class.Trainee
Can we conclude from this that accessing a property is faster on x64 then accessing a field!!! Cause that is a hell of a surprise to me how this can be?Gris
I wouldn't conclude that, because: "Even with 50mil iterations, the numbers still bounce around +/- 50 or so millisec so I wouldn't read too much into the minor differences."Trainee
D
23

Point Andrew Hare made about performance loss when you perform is check and then cast was valid but since C# 7.0 we can do is check with pattern match to avoid additional cast later on:

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

Further more if you need to check between multiple types C# 7.0 pattern matching constructs now allow you to do switch on types:

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

You can read more about pattern matching in C# in documentation here.

Doiron answered 4/6, 2017 at 9:28 Comment(4)
A valid solution, for sure, but this C# pattern-matching feature makes me sad, when it encourages "feature-envy" code like this. Surely we should be striving for encapsulation of logic where only the derived objects "know" how to calculate their own area, and then they just return the value?Campeche
SO needs filter buttons (on the question) for answers that apply to newer versions of a framework, platform etc. This answer forms the basis of the correct one for C# 7.Opalopalesce
@Campeche OOP ideals get thrown out of the window when you're working with types/classes/interfaces that you don't control. This approach is also useful for when handling the result of a function that can return one-of-many values of completely different types (because C# still doesn't support union-types yet - you can use libraries like OneOf<T...> but they have major shortcomings).Simper
@Campeche To rephrase what Dai said: If the class hierarchy you're working with is from a library that you don't control the source of, you can't add a new virtual function. Pattern matching solves this by allowing you to create a new function that can change its behaviour based on the type without having to modify the existing code. It also provides a much simpler way to do what would ordinarily require the hacky, boilerplate-intensive visitor pattern.Limpet
V
21

I did a performance comparsion on two possibilities of type comparison

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

The result is: Using "is" is about 10x faster !!!

Output:

Time for Type-Comparison: 00:00:00.456

Time for Is-Comparison: 00:00:00.042

My Code:

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);
        }
    }
}
Voluptuary answered 17/7, 2012 at 8:46 Comment(2)
Try to always use something like BenchmarkDotNet instead of rolling your own, because you're likely to get caught by warmup effects etc.Anaesthetize
With .NET 6.0.1, I see is X being much slower than GetType() == typeof(X). gist.github.com/Zastai/1fbaa1e5f290ee46999361adbca6424d for the code usedAnaesthetize
G
18

Andrew is correct. In fact with code analysis this gets reported by Visual Studio as an unnecessary cast.

One idea (without knowing what you're doing is a bit of a shot in the dark), but I've always been advised to avoid checking like this, and instead have another class. So rather than doing some checks and having different actions depending on the type, make the class know how to process itself...

e.g. Obj can be ISpecialType or IType;

both of them have a DoStuff() method defined. For IType it can just return or do custom stuff, whereas ISpecialType can do other stuff.

This then completely removes any casting, makes the code cleaner and easier to maintain, and the class knows how to do it's own tasks.

Gettings answered 26/3, 2009 at 16:15 Comment(3)
Yes, since all I am going to do if the type tests true is call a certain interface method on it, I could just move that interface method into the base class and have it do nothing by default. That might be more elegant than creating a virtual function to test type.Fescennine
I did a similar test to Binary Worrier after abatishchev's comments and found only 60ms difference over 10,000,000 itterations.Gettings
Wow, thanks for the help. I suppose I'll stick to using the type checking operators for now then, unless it seems appopriate to reorganize the class structure. I'll use the 'as' operator as Andrew suggested since I don't want to cast redundantly.Fescennine
G
5

In case anyone is wondering, I've made tests in Unity engine 2017.1, with scripting runtime version .NET4.6(Experimantal) on a notebook with i5-4200U CPU. Results:

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

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

Groundspeed answered 11/8, 2017 at 10:38 Comment(3)
Article link is dead.Selby
@James link revived.Groundspeed
Good stuff - but I didn't downvote you (actually I upvoted anyway); In case you are wondering. :)Selby
C
-3

I've always been advised to avoid checking like this, and instead have another class. So rather than doing some checks and having different actions depending on the type, make the class know how to process itself...

e.g. Obj can be ISpecialType or IType;

both of them have a DoStuff() method defined. For IType it can just return or do custom stuff, whereas ISpecialType can do other stuff.

This then completely removes any casting, makes the code cleaner and easier to maintain, and the class knows how to do it's own tasks.

Calcify answered 3/7, 2014 at 17:40 Comment(2)
This does not answer the question. Anyway, classes may not always know how to process themselves due to lack of context. We apply similar logic to exception handling when we allow exceptions to go up the call chain until some method/function has enough context to handle the errors.Ambitendency
Not only this "answer" does not answer the question, but also, it is just a mindless copy-paste of part of Ian's answer, made about 5 years after Ian's answer.Centre

© 2022 - 2024 — McMap. All rights reserved.