До недавнего времени мой ответ был бы очень близок к ответу Джона Скита. Тем не менее, я недавно начал проект, в котором использовались хеш-таблицы степени двойки, то есть хеш-таблицы, где размер внутренней таблицы равен 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();
}
Он также принимает начальные значения, поэтому, если вам нужно иметь дело с ненадежным вводом и хотите защитить от атак Hash 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 битах, хотя все еще достаточно быстро, чтобы быть разумным выбором.
GetHashCode
. Я надеюсь, что это будет полезно для других. Руководство и правила для GetHashCode, написанные Эриком Липпертом