Why is Equals for C# 9 records so slow?

Viewed 724

I used the following benchmark:

void Main()
{
    const int reps = 100000000;
    DateTime now1 = DateTime.Now, now2 = now1/*.AddSeconds(1)*/;
    var tuple1 = (2, "wer", now1);
    var tuple2 = (2, "wer", now2);
    var myClass1 = new MyClass(2, "wer", now1);
    var myClass2 = new MyClass(2, "wer", now2);
    var slowStruct1 = new SlowStruct(2, "wer", now1);
    var slowStruct2 = new SlowStruct(2, "wer", now2);
    var record1 = new MyRecord(2, "wer", now1);
    var record2 = new MyRecord(2, "wer", now2);

    var sw = new Stopwatch();
    int total = 0;

    //empty (22)
    sw.Restart();
    for (int i = 0; i < reps; i++)
    {
        total++;
    }
    sw.ElapsedMilliseconds.Dump("empty");

    //times are in ms: (same date, different date)
    
    //Tuple (327,390)
    sw.Restart();
    for (int i = 0; i < reps; i++)
    {
        if (tuple1.Equals(tuple2))
            total++;
    }
    sw.ElapsedMilliseconds.Dump("Tuple");

    //Tuple Manual (135,155)
    sw.Restart();
    for (int i = 0; i < reps; i++)
    {
        if (tuple1.Item1 == tuple2.Item1 && tuple1.Item2 == tuple2.Item2 && tuple1.Item3 == tuple2.Item3)
            total++;
    }
    sw.ElapsedMilliseconds.Dump("Tuple Manual");

    //MyClass (170,201) class & (readonly) struct have identical performance.
    sw.Restart();
    for (int i = 0; i < reps; i++)
    {
        if (myClass1.Equals(myClass2))
            total++;
    }
    sw.ElapsedMilliseconds.Dump("MyClass");

    //SlowStruct (32231,31694) readonly does not help at all
    //total = 0;
    //sw.Restart();
    //for (int i = 0; i < reps; i++)
    //{
    //    if (slowStruct1.Equals(slowStruct2))
    //        total++;
    //    slowStruct1.Item1++;
    //    slowStruct2.Item1++;
    //}
    //sw.ElapsedMilliseconds.Dump("SlowStruct");

    //record (1126,1142)
    sw.Restart();
    for (int i = 0; i < reps; i++)
    {
        if (record1.Equals(record2))
            total++;
    }
    sw.ElapsedMilliseconds.Dump("record");

    total.Dump();

}

class MyClass : IEquatable<MyClass>
{
    public int Item1;
    public string Item2;
    public DateTime Item3;

    public MyClass(int item1, string item2, DateTime item3) => (Item1, Item2, Item3) = (item1, item2, item3);

    public bool Equals(MyClass other) => Item1 == other.Item1 && Item2 == other.Item2 && Item3 == other.Item3;
    public override int GetHashCode()
    {
        var h = 17;
        h = h * 31 + Item1.GetHashCode();
        h = h * 31 + Item2.GetHashCode();
        h = h * 31 + Item3.GetHashCode();
        return h;
    }
    public override bool Equals(object obj) => throw new NotImplementedException();
}

struct SlowStruct
{
    public int Item1;
    public string Item2;
    public DateTime Item3;

    public SlowStruct(int item1, string item2, DateTime item3) => (Item1, Item2, Item3) = (item1, item2, item3);

}

record MyRecord(int Item1, string Item2, DateTime Item3);

Ran it in LINQPad, with compiler optimizations on. (Similar ratio when it's off.)

Using MyClass took 170ms, and using record took 1126ms. Subtracting the foreach overhead of 22ms, gives a ratio of (1126-22)/(170-22)=7.5.

Meaning that calling .Equals on my own class is about 7 times faster than calling .Equals on the record.

Why couldn't the guys who made C# make records as fast as my own class?

0 Answers
Related