Эффективность использования IEqualityComparer в словаре vs HashCode и Equals()
Название довольно ясно, я думаю.
Мне было интересно, есть ли определенные издержки при использовании IEqualityComparer
в Dictionary<K,V>
, как все это работает при предоставлении одного?
Спасибо
Ответы
Ответ 1
Быстрее?
Исходя из перспективы gamedev, если ваш ключ - тип значения (struct, primitive, enum и т.д.), обеспечивающий ваш собственный EqualityComparer<T>
значительно быстрее - из-за того, что значение EqualityComparer<T>.Default
содержит значение.
Как пример в реальном мире, образец Billboard Managed DirectX, используемый для запуска на ~ 30% скорости версии С++; где все остальные образцы работали на ~ 90%. Причина этого заключалась в том, что рекламные щиты сортировались с использованием сравнения по умолчанию (и, следовательно, в коробке), поскольку, как оказалось, 4MB данных копируется вокруг каждого кадра.
Как это работает?
Dictionary<K,V>
предоставит EqualityComparer<T>.Default
себе через конструктор по умолчанию. То, что делает компаратор равенства по умолчанию (в основном, заметьте, сколько бокса происходит):
public void GetHashCode(T value)
{
return ((object)value).GetHashCode();
}
public void Equals(T first, T second)
{
return ((object)first).Equals((object)second);
}
Почему я когда-либо использовал его?
Весьма распространено видеть этот тип кода (при попытке использовать нечувствительные к регистру ключи):
var dict = new Dictionary<string, int>();
dict.Add(myParam.ToUpperInvariant(), fooParam);
// ...
var val = dict[myParam.ToUpperInvariant()];
Это действительно расточительно, лучше просто использовать StringComparer для конструктора:
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
Является ли это быстрее (сокращение)?
В этом конкретном сценарии это происходит намного быстрее, потому что сопоставление порядковых строк - это самый быстрый тип сравнения строк, который вы можете сделать. Быстрый тест:
static void Main(string[] args)
{
var d1 = new Dictionary<string, int>();
var d2 = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
d1.Add("FOO", 1);
d2.Add("FOO", 1);
Stopwatch s = new Stopwatch();
s.Start();
RunTest1(d1, "foo");
s.Stop();
Console.WriteLine("ToUpperInvariant: {0}", s.Elapsed);
s.Reset();
s.Start();
RunTest2(d2, "foo");
s.Stop();
Console.WriteLine("OrdinalIgnoreCase: {0}", s.Elapsed);
Console.ReadLine();
}
static void RunTest1(Dictionary<string, int> values, string val)
{
for (var i = 0; i < 10000000; i++)
{
values[val.ToUpperInvariant()] = values[val.ToUpperInvariant()];
}
}
static void RunTest2(Dictionary<string, int> values, string val)
{
for (var i = 0; i < 10000000; i++)
{
values[val] = values[val];
}
}
// ToUpperInvariant: 00:00:04.5084119
// OrdinalIgnoreCase: 00:00:02.1211549
// 2x faster.
Бронирование
Можно устранить накладные расходы бокса, реализовав интерфейс на структуре (например, IEquatable<T>
). Тем не менее, есть много удивительных правил, когда бокс происходит в этих обстоятельствах, поэтому я бы рекомендовал использовать сопряженный интерфейс (например, IEqualityComparer<T>
в этом случае), если это вообще возможно.
Ответ 2
Джонатан имеет отличный ответ, который указывает, как с помощью правильного сравнения сравнений производительность улучшается, а Джон уточняет в его отличный ответ, что Dictionary<K, V>
всегда использует IEqualityComparer<T>
, который EqualityComparer<T>.Default
, если вы не укажете другое.
То, что я хотел бы затронуть, - это роль интерфейса IEquatable<T>
, когда вы используете сопоставитель равенства по умолчанию.
Когда вы вызываете EqualityComparer<T>.Default
, он использует кешированный компаратор, если он есть. Если это первый раз, когда вы используете сопоставитель равенства по умолчанию для этого типа, он вызывает метод под названием CreateComparer
и кэширует результат для последующего использования. Ниже приведена упрощенная и упрощенная реализация CreateComparer
в .NET 4.5:
var t = (RuntimeType)typeof(T);
// If T is byte,
// return a ByteEqualityComparer.
// If T implements IEquatable<T>,
if (typeof(IEquatable<T>).IsAssignableFrom(t))
return (EqualityComparer<T>)
RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter(
(RuntimeType)typeof(GenericEqualityComparer<int>), t);
// If T is a Nullable<U> where U implements IEquatable<U>,
// return a NullableEqualityComparer<U>
// If T is an int-based Enum,
// return an EnumEqualityComparer<T>
// Otherwise return an ObjectEqualityComparer<T>
Но что это означает для типов, реализующих IEquatable<T>
?
Здесь определение GenericEqualityComparer<T>
:
internal class GenericEqualityComparer<T> : EqualityComparer<T>
where T: IEquatable<T>
// ...
Магия случается в ограничении общего типа (where T : IEquatable<T>
part), потому что использование этого не связано с боксом, если T
- тип значения, здесь нет каста вроде (IEquatable<T>)T
, что является основным преимуществом дженериков.
Итак, скажем, нам нужен словарь, который отображает целые числа в строки.
Что произойдет, если мы инициализируем один, используя конструктор по умолчанию?
var dict = new Dictionary<int, string>();
- Мы знаем, что словарь использует
EqualityComparer<T>.Default
, если мы не укажем другое.
- Мы знаем, что
EqualityComparer<int>.Default
проверяет, реализует ли int IEquatable<int>
.
- Мы знаем, что
int
(Int32
) реализует IEquatable<Int32>
.
Первый вызов EqualityComparer<T>.Default
будет создавать и кэшировать общий сопоставитель, который может занять немного, но при инициализации он сильно набрал GenericEqualityComparer<T>
, и использование этого не приведет к боксу или ненужным накладным расходам.
И все последующие вызовы EqualityComparer<T>.Default
вернут кешированный компаратор, что означает, что служебные данные инициализации одноразовые только для каждого типа.
Итак, что это значит?
- Внесите собственный сопоставитель сравнений, если
T
не реализует IEquatable<T>
или, его реализация IEquatable<T>
не делает того, что вы хотите.
(т.е. obj1.Equals(obj2)
не дает желаемого результата.)
Использование StringComparer
в ответе Джонатана - отличный пример, почему вы должны указать пользовательский сопоставитель сравнений.
- Не реализуйте собственный сопоставитель сравнений ради производительности, если
T
реализует IEquatable<T>
и, реализация IEquatable<T>
делает то, что вы хотите сделать.
(т.е. obj1.Equals(obj2)
дает желаемый результат).
В последнем случае используйте EqualityComparer<T>.Default
.
Ответ 3
Dictionary<,>
всегда использует IEqualityComparer<TKey>
- если вы его не передаете, он использует EqualityComparer<T>.Default
. Таким образом, эффективность будет зависеть от того, насколько эффективна ваша реализация по сравнению с EqualityComparer<T>.Default
(которая просто делегирует Equals
и GetHashCode
).