До недавнего времени мой ответ был очень близок к тому, что сказал здесь Джон Скит. Однако недавно я начал проект, в котором использовались хэш-таблицы степени двойки, то есть хеш-таблицы, в которых размер внутренней таблицы составляет 8, 16, 32 и т. Д. Есть веская причина для предпочтения размеров простых чисел, но есть также есть некоторые преимущества для размеров, рассчитанных со степенью двойки.
И это в значительной степени отстой. Итак, после небольших экспериментов и исследований я начал повторно хешировать свои хэши следующим образом:
public static int ReHash(int source)
{
unchecked
{
ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
ulong d = 0xE2ADBEEFDEADBEEF ^ c;
ulong a = d += c = c << 15 | c >> -15;
ulong b = a += d = d << 52 | d >> -52;
c ^= b += a = a << 26 | a >> -26;
d ^= c += b = b << 51 | b >> -51;
a ^= d += c = c << 28 | c >> -28;
b ^= a += d = d << 9 | d >> -9;
c ^= b += a = a << 47 | a >> -47;
d ^= c += b << 54 | b >> -54;
a ^= d += c << 32 | c >> 32;
a += d << 25 | d >> -25;
return (int)(a >> 1);
}
}
А потом моя хеш-таблица со степенью двойки перестала быть отстойной.
Это меня беспокоило, потому что вышеперечисленное не должно работать. Или, точнее, это не должно работать, если исходный GetHashCode()
не был плохим каким-то определенным образом.
Повторное смешивание хэш-кода не может улучшить отличный хэш-код, потому что единственный возможный эффект - это то, что мы вводим еще несколько коллизий.
Повторное смешивание хэш-кода не может улучшить ужасный хеш-код, потому что единственный возможный эффект - мы изменим, например. большое количество коллизий по значению 53 с большим количеством значений 18,3487,291.
Повторное смешивание хэш-кода может только улучшить хэш-код, который, по крайней мере, неплохо справился с предотвращением абсолютных коллизий во всем диапазоне (2 32 возможных значения), но плохо с избежанием коллизий при уменьшении по модулю для фактических использовать в хеш-таблице. В то время как более простой модуль таблицы степени двойки сделал это более очевидным, он также имел негативный эффект с более распространенными таблицами простых чисел, что было не так очевидно (дополнительная работа по перехешированию перевесила бы выгоду , но польза все равно будет).
Изменить: я также использовал открытую адресацию, что также увеличило бы чувствительность к столкновениям, возможно, больше, чем тот факт, что это была степень двойки.
И что ж, меня беспокоит, насколько string.GetHashCode()
реализации в .NET (или изучите здесь) можно улучшить таким образом (в порядке тестов, выполняющихся примерно в 20-30 раз быстрее из-за меньшего коллизий) и более тревожно, насколько мои собственные хэш-коды могут быть улучшены (намного больше).
Все реализации GetHashCode (), которые я кодировал в прошлом и которые действительно использовались в качестве основы для ответов на этом сайте, были намного хуже, чем я думал. В большинстве случаев он был «достаточно хорош» для большинства применений, но мне хотелось чего-то получше.
Поэтому я отложил этот проект в сторону (в любом случае это был любимый проект) и начал думать, как быстро создать хороший, хорошо распределенный хеш-код в .NET.
В конце концов я остановился на переносе SpookyHash на .NET. Действительно, приведенный выше код представляет собой ускоренную версию использования SpookyHash для создания 32-разрядного вывода из 32-разрядного ввода.
SpookyHash - это не очень хорошо запоминающийся фрагмент кода. Мой порт еще хуже, потому что я много его вручную встроил для лучшей скорости *. Но для этого и нужно повторное использование кода.
Затем я отложил этот проект в сторону, потому что точно так же, как исходный проект поставил вопрос о том, как создать лучший хэш-код, этот проект поставил вопрос о том, как создать лучший .NET memcpy .
Затем я вернулся и произвел множество перегрузок, чтобы легко передать почти все собственные типы (кроме decimal
†) в хэш-код.
Это быстро, за что Боб Дженкинс заслуживает наибольшей похвалы, потому что его исходный код, с которого я портировал, еще быстрее, особенно на 64-битных машинах, для которых алгоритм оптимизирован ‡.
Полный код можно увидеть на странице https://bitbucket.org/JonHanna/spookilysharp/src, но считайте, что приведенный выше код является его упрощенной версией.
Однако, поскольку он уже написан, его легче использовать:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
Он также принимает начальные значения, поэтому, если вам нужно иметь дело с ненадежным вводом и вы хотите защитить себя от хэш-атак DoS, вы можете установить начальное значение на основе времени безотказной работы или аналогичного, и сделать результаты непредсказуемыми для злоумышленников:
private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
//produce different hashes ever time this application is restarted
//but remain consistent in each run, so attackers have a harder time
//DoSing the hash tables.
var hash = new SpookyHash(hashSeed0, hashSeed1);
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
* Большой сюрприз в этом заключается в том, что ручное встраивание метода поворота, возвращающего (x << n) | (x >> -n)
, улучшило ситуацию. Я был бы уверен, что джиттер для меня это встроил, но профилирование показало обратное.
† decimal
не является родным с точки зрения .NET, хотя он исходит из C #. Проблема в том, что его собственная GetHashCode()
считает точность важной, а ее собственная Equals()
- нет. Оба варианта допустимы, но не смешаны таким образом. При реализации вашей собственной версии вам нужно выбрать одну или другую, но я не знаю, что вам нужно.
‡ Для сравнения. При использовании в строке SpookyHash на 64 битах значительно быстрее, чем string.GetHashCode()
на 32 битах, что немного быстрее, чем string.GetHashCode()
на 64 битах, что значительно быстрее, чем SpookyHash на 32 битах, хотя все же достаточно быстро, чтобы быть разумным выбором.
person
Jon Hanna
schedule
14.01.2014
GetHashCode
. Я надеюсь, что это будет полезно для других. Рекомендации и правила для GetHashCode, написанный Эриком Липпертом - person rene   schedule 23.03.2012GetHashCode()
используется во многих реализацияхEquals()
. Вот что я имел в виду в этом заявлении.GetHashCode()
внутриEquals()
часто используется как ярлык для определения неравенства, потому что, если два объекта имеют разный хэш-код, они должны быть объектами, которые не равны, а остальная часть проверка на равенство не требуется. - person bitbonk   schedule 03.09.2015GetHashCode()
, иEquals()
должны просматривать все поля обоих объектов (Equals должен это сделать, если хэш-коды равны или не проверены). Из-за этого вызовGetHashCode()
внутриEquals()
часто является избыточным и может снизить производительность.Equals()
также может иметь возможность короткого замыкания, что делает его намного быстрее - однако в некоторых случаях хэш-коды могут быть кэшированы, что делаетGetHashCode()
проверку более быстрой и полезной. Дополнительную информацию см. В этом вопросе. - person NotEnoughData   schedule 02.04.2017