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?