C# Tip: Override Equals() on Value Types for Better Performance

In C# we have the option of using value types (structs) or reference types (classes). The distinction is important and you should consider carefully which kind of object your types should be. In this post I won't go into detail about how to make this choice, but I'll give you a quick tip for improving performance in your application.

The default implementation of instance Equals() depends on your object type. Reference types inherit System.Object's default implementation, which is a simple object identity (ReferenceEquals()) check. By default, reference types will return true on instance Equals() if and only if they are the same object instance.

Value types, on the other hand, inherit from System.ValueType. This class overrides System.Object.Equals with an implementation that uses reflection to check each field in the struct for equality (using instance Equals()). As you may know, reflection is a powerful tool but is also very inefficient. To drive the point home I wrote a quick program to check the difference in performance between the default equality comparison and a custom implementation that simply compares the fields of the struct, but without using reflection to inspect them.

  class Program
    {
        static void Main(string[] args)
        {
            Address a1 = new Address()
            {
                StreetName = "Some Street",
                StreetNumber = 1
            };
            Address a2 = new Address()
            {
                StreetName = "Other Street",
                StreetNumber = 2
            };
            AddressWithEquals a3 = new AddressWithEquals()
            {
                StreetName = "Some Street",
                StreetNumber = 1
            };
            AddressWithEquals a4 = new AddressWithEquals()
            {
                StreetName = "Other Street",
                StreetNumber = 2
            };
            StringBuilder builder = new StringBuilder();
            builder.Append("Iterations,WithoutEquals,WithEquals\n");
            for (int i = 5; i < 10; i++)
            {
                int iterations = (int)Math.Pow(10.0, (double)i);
                bool b;
                DateTime start = DateTime.Now;
                for (int j = 0; j < iterations; j++)
                {
                    b = a1.Equals(a2);
                }
                double finish1 = (DateTime.Now - start).TotalMilliseconds;
                start = DateTime.Now;
                for (int j = 0; j < iterations; j++)
                {
                    b = a3.Equals(a4);
                }
                double finish2 = (DateTime.Now - start).TotalMilliseconds;
                builder.Append(String.Format(&quot;{0},{1},{2}\n&quot;, iterations,finish1,finish2));
            }
            StreamWriter writer = new StreamWriter(@&quot;C:\Users\Martin\Desktop\structOutput.txt&quot;);
            writer.Write(builder.ToString());
            writer.Close();
        }
    }
    struct Address
    {
        public string StreetName { get; set; }
        public int StreetNumber { get; set; }
    }
    struct AddressWithEquals
    {
        public string StreetName { get; set; }
        public int StreetNumber { get; set; }
        public override bool Equals(object obj)
        {
            if (obj == null) return false;
            if (obj.GetType() != this.GetType()) return false;
            AddressWithEquals other = (AddressWithEquals)obj;
            return other.StreetName == this.StreetName &amp;&amp;
                   other.StreetNumber == this.StreetNumber;
        }
    }

What I found was that using your own implementation was consistently as much as 7 times faster than using the default implementation. The ratio between the two running times was nearly perfectly constant as the number of iterations increased, and as I varied the number of fields for comparison.

Here are a couple of graphs showing some of my results. The data is identical, but the second graph uses a logarithmic scale on the Y-axis to match that on the X-axis. The difference in performance is approximately 6-fold, sometimes as high as 7-fold. You can see how perfectly it scales with iterations. The scaling with the number of fields is the same.

Equality checking is a fundamental operation. It is often said that you should never optimize your code until you find a performance bottleneck. In my opinion equality comparison of structs in C# is an exception - always override .Equals() with your structs.