Generally there are two available options in a case like this:

  1. Use 'is' check to check for type for every element of a list.
  2. Store the type as an enum inside the base class and compare that.

I opted for the first approach, but it was really bugging me since we've always been told any kind of type checking is bad for performance. In fact, I've believed this is at least as costly as a virtual call. I've came up on this page, and the results presented there clearly showed the is check isn't bad. Since these measurements were in .NET, and in Unity we're working on Mono, I've had to test this for myself.

I've used the code below to make the tests:

using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;

namespace ES.Testers {
    public class SpeedTest : MonoBehaviour {
        public int iterations = 10000;
        public int elementsCount = 1000;

        void Start() {
            bool dummy = false;

            IsTest(ref dummy);
            EnumTest(ref dummy);
            VirtualCallTest(ref dummy);
            LocalCallTest(ref dummy);
            GetTypeTest(ref dummy);

            UnityEngine.Debug.Log("Protection from inlining dummy: " + dummy);
        }

        void IsTest(ref bool dummy) {
            List<IsBase> list = new List<IsBase>();
            for(int i = 0; i < elementsCount; i++) {
                IsBase newElement;
                if(Random.Range(0, 1) < 0.5) newElement = new IsDerived1();
                else newElement = new IsDerived2();
                list.Add(newElement);
            }

            var sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < iterations; i++) {
                for(int j = 0; j < elementsCount; j++) {
                    dummy = list[j] is IsDerived1;
                }
            }
            sw.Stop();

            UnityEngine.Debug.LogFormat("The 'is' test took {0}ms for {1} iterations.", sw.ElapsedMilliseconds, iterations);
        }

        void EnumTest(ref bool dummy) {
            List<EnumBase> list = new List<EnumBase>();
            for(int i = 0; i < elementsCount; i++) {
                EnumBase newElement;
                if(Random.Range(0, 1) < 0.5) newElement = new EnumDerived1();
                else newElement = new EnumDerived2();
                list.Add(newElement);
            }

            var sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < iterations; i++) {
                for(int j = 0; j < elementsCount; j++) {
                    dummy = list[j].classType == ClassType.First;
                }
            }
            sw.Stop();

            UnityEngine.Debug.LogFormat("The 'Enum' test took {0}ms for {1} iterations.", sw.ElapsedMilliseconds, iterations);
        }

        void VirtualCallTest(ref bool dummy) {
            List<VirtualBase> list = new List<VirtualBase>();
            for(int i = 0; i < elementsCount; i++) {
                VirtualBase newElement;
                if(Random.Range(0, 1) < 0.5) newElement = new VirtualDerived1();
                else newElement = new VirtualDerived2();
                list.Add(newElement);
            }

            var sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < iterations; i++) {
                for(int j = 0; j < elementsCount; j++) {
                    dummy = list[j].myType == ClassType.First;
                }
            }
            sw.Stop();

            UnityEngine.Debug.LogFormat("The 'Virtual Call' test took {0}ms for {1} iterations.", sw.ElapsedMilliseconds, iterations);
        }

        void LocalCallTest(ref bool dummy) {
            var sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < iterations; i++) {
                for(int j = 0; j < elementsCount; j++) {
                    dummy = LocalMethod();
                }
            }
            sw.Stop();

            UnityEngine.Debug.LogFormat("The 'Local Call' test took {0}ms for {1} iterations.", sw.ElapsedMilliseconds, iterations);
        }

        void GetTypeTest(ref bool dummy) {
            List<IsBase> list = new List<IsBase>();
            for(int i = 0; i < elementsCount; i++) {
                IsBase newElement;
                if(Random.Range(0, 1) < 0.5) newElement = new IsDerived1();
                else newElement = new IsDerived2();
                list.Add(newElement);
            }

            var sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < iterations; i++) {
                for(int j = 0; j < elementsCount; j++) {
                    dummy = list[j].GetType() == typeof(IsDerived1);
                }
            }
            sw.Stop();

            UnityEngine.Debug.LogFormat("The 'GetType' test took {0}ms for {1} iterations.", sw.ElapsedMilliseconds, iterations);
        }

        bool LocalMethod() {
            return false;
        }
    }

    abstract class IsBase { }

    class IsDerived1 : IsBase { }

    class IsDerived2 : IsBase { }

    enum ClassType { First, Second }

    abstract class EnumBase {
        public ClassType classType;
    }

    class EnumDerived1 : EnumBase {
        public EnumDerived1() { classType = ClassType.First; }
    }

    class EnumDerived2 : EnumBase {
        public EnumDerived2() { classType = ClassType.Second; }
    }

    abstract class VirtualBase {
        public abstract ClassType myType { get; }
    }

    class VirtualDerived1 : VirtualBase {
        public override ClassType myType {
            get { return ClassType.First; }
        }
    }

    class VirtualDerived2 : VirtualBase {
        public override ClassType myType {
            get { return ClassType.Second; }
        }
    }
}

Notes on the code:

The cost of iterating the loop and assignment is included in the tests. This is on purpose, because without these elements we would lose the context and get into 'relatively comparing differences' territory.
U used the dummy variable as ref to make sure the compiler does not exclude the variable which is unused after assignment.
The random distribution of elements in a sequence is significant. I did a run where all elements are the same and results are very different - all calls are 70% cheaper. I am guessing this is CPU's branch prediction at work.

  Run 1 Run 2 Run 3 Average Relative
Method 116 118 118 117.33 1.0
is 240 242 243 241.67 2.06
enum 136 135 147 139.33 1.19
V-Call 282 304 297 294.33 2.51
GetType 278 255 295 276 2.35

The tests were done on an underpoewered notebook, with an i5-4200U, with Balanced Power Saving Options, using Unity 2017.1 with the scripting runtime version .NET4.6(Experimantal).

Conclusions:

  • Enum check is pretty cheap, but not that cheap. I've read somewhere the enum check test is actually quite a bit more expensive in .NET then you'd expect. It is still the best bet. 
  • is is surprisingly cheap. It is definitely much cheaper then GetType(). It is also cheaper than a virtual method call.
  • Virtual calls are 2.5x more expensive than local calls.
  • GetType is not the most expensive. typeof() part of this test should have no cost as the exact type is substituted at compile time. I was expecting this call to be more expensive.

 


If you're finding this article helpful, consider our asset Dialogical on the Unity Asset store for your game dialogues.