The speed of value type comparison in C#

The speed of value type comparison in C#

Introduction

In C#, there is a type named ValueType. Numeric types such as int, double are all derived from it. Sometimes we may want to compare two ValueType object (the two object are actually the same numeric type). In some conditions, though we can know what the real numeric type the object is, we may not want to perform a casting and wish that there is a way to just compare them bit by bit.

Well, to the best of my knowledge, there are two ways to do the comparison, which are Object.Equals, and casting.

Benchmark

We use the code below to run the benchmark. The experiment environment is Intel i7-12700, 64bits, Windows 11, .NET 6.

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class Program
{
    public static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
        Console.WriteLine(summary);
    }
}

public class UnitTestCompare
{
    public static List<Tuple<ValueType, ValueType>> data = new List<Tuple<ValueType, ValueType>>();
    static UnitTestCompare()
    {
        for(int i = 0; i < 10000; i++)
        {
            data.Add(new Tuple<ValueType, ValueType>(
                new Random(DateTime.Now.Millisecond).Next(-9999, 9999), 
                new Random(DateTime.Now.Millisecond).Next(-9999, 9999)
                ));
        }
    }
    [Benchmark]
    public void ObjectEqual()
    {
        bool res;
        foreach (var (a, b) in data)
        {
            res = a.Equals(b);
        }
    }
    [Benchmark]
    public void CastingEqual()
    {
        bool res;
        foreach (var (a, b) in data)
        {
            res = (int)a == (int)b;
        }
    }
}

The result is listed below.

|        Method |      Mean |     Error |    StdDev |
|-------------- |----------:|----------:|----------:|
|   ObjectEqual | 26.425 us | 0.5037 us | 0.5800 us |
| CastingEqual  |  9.449 us | 0.1850 us | 0.2272 us |

Obviously, Casting is much faster than Object.Equals. But why?

Principle

I read the source code of Object.Equals and here is its implementation.

        public virtual bool Equals(object? obj)
        {
            return RuntimeHelpers.Equals(this, obj);
        }

        public static bool Equals(object? objA, object? objB)
        {
            if (objA == objB)
            {
                return true;
            }
            if (objA == null || objB == null)
            {
                return false;
            }
            return objA.Equals(objB);
        }

It asks the derived class for an implementation, otherwise it use the default comparison method.

Then, let's find its override in Int32, as is shown below.

        public override bool Equals([NotNullWhen(true)] object? obj)
        {
            if (!(obj is int))
            {
                return false;
            }
            return m_value == ((int)obj).m_value;
        }

Here is it! The override also cast the object to Int32 while there are some extra instructions, which slow down the speed.

Futher Look

After that, I did an extra experiment, adding the code below to run benchmark.

    [Benchmark]
    public void LeftEqual()
    {
        bool res;
        foreach (var (a, b) in data)
        {
            res = a.Equals((int)b);
        }
    }
    [Benchmark]
    public void RightEqual()
    {
        bool res;
        foreach (var (a, b) in data)
        {
            res = ((int)a).Equals(b);
        }
    }

I want to verify that which part consumes more time. But something strange happened, as is listed below.

|        Method |      Mean |     Error |    StdDev |
|-------------- |----------:|----------:|----------:|
|   ObjectEqual | 26.989 us | 0.3632 us | 0.3398 us |
|     LeftEqual | 39.699 us | 0.4720 us | 0.7622 us |
|    RightEqual |  9.938 us | 0.1789 us | 0.3360 us |
| CastingEqual  |  8.891 us | 0.1773 us | 0.2242 us |

The LeftEqual is even slower than ObjectEqual! It shocked me because it's 13 us slower than it, which is bigger than the time of CastingEqual.

The reason is that there is actually two extra casting. The first time is in our code, we write (int)b. The second time is the construction of the parameter of Object.Equals(). It accepts a parameter of type Object, so the "int b" was casted to Object again. In this way, the average casting time is about 6.5 us, which well matches the results above.

Conclusion

Somtimes, we may think it's ugly to casting an object to a certain type and choose some other ways to complete the things. However, sometimes, especially in the condition that high performance is required, casting may not be a bad choice.