HashSet<T>.Add сначала сравнивает результаты GetHashCode. Если они равны, он вызывает Equals.
Теперь, я понимаю, что для реализации GetHashCode нужно что-то делать с полями объекта. Простую примерную реализацию можно найти в Каков наилучший алгоритм для переопределенного System.Object.GetHashCode?.
В моем тестировании, сравнивающем как 1.000.000 пар объектов, заполненных случайными данными, производительность более или менее одинакова между ними. GetHashCode реализуется как в связанном примере, Equals просто вызывает Equals для всех полей. Итак, почему нужно использовать GetHashCode над Equals?