Донедавна моя відповідь була б дуже близькою до Джона Скіта. Однак я нещодавно розпочав проект, в якому використовували хеш-таблиці потужності з двох потужностей, тобто хеш-таблиці, де розмір внутрішньої таблиці становить 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.
Потім я повернувся і створив безліч перевантажень, щоб легко вводити майже 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, написані Еріком Ліппертом