Які цілі хеш-функції хороші, що приймає цілий хеш-ключ?


Відповіді:


47

Мультиплікативний метод Кнута:

hash(i)=i*2654435761 mod 2^32

Загалом, ви повинні вибрати множник, який відповідає вашому розміру хешу ( 2^32у прикладі) і не має з ним загальних факторів. Таким чином хеш-функція рівномірно покриває весь ваш хеш-простір.

Редагувати: Найбільшим недоліком цієї хеш-функції є те, що вона зберігає подільність, тому якщо ваші цілі числа діляться на 2 або на 4 (що не є рідкістю), їхні хеші теж будуть. Це проблема в хеш-таблицях - ви можете отримати лише 1/2 або 1/4 відра.


36
Це дійсно погана хеш-функція, хоч і прив’язана до відомого імені.
Seun Osewa

5
Це зовсім не погана хеш-функція, якщо вона використовується з простими розмірами таблиці. Також він призначений для закритого хешування. Якщо значення хешу розподілені не рівномірно, мультипликативне хешуваннявання гарантує, що зіткнення з одним значенням навряд чи можуть "порушити" елементи з іншими хеш-значеннями.
Паоло Бонзіні

11
Для допитливих ця константа обрана розміром хешу (2 ^ 32), поділеним на Phi
awdz9nld

7
Паоло: Метод Кнута "поганий" в тому сенсі, що він не
лавинує

9
При більш детальному огляді виявляється, що 2654435761 - це насправді перше число. Тож, мабуть, саме тому було обрано замість 2654435769.
karadoc

149

Я виявив, що наступний алгоритм забезпечує дуже хороший статистичний розподіл. Кожен вхідний біт впливає на кожен вихідний біт з приблизно 50% вірогідністю. Сутичок немає (кожен вхід призводить до різного виходу). Алгоритм швидкий, за винятком випадків, коли в процесорі немає вбудованого цілого числа множення. C код, припускаючи , що intце 32 біт (для Java, замінити >>з >>>і видалити unsigned):

unsigned int hash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = (x >> 16) ^ x;
    return x;
}

Магічне число було розраховано за допомогою спеціальної багатопотокової програми тестування, яка працювала протягом багатьох годин, яка обчислює ефект лавини (кількість вихідних бітів, які змінюються, якщо змінено один вхідний біт; в середньому має бути майже 16), незалежність зміни вихідного біта (вихідні біти не повинні залежати один від одного), а також ймовірність зміни кожного вихідного біта, якщо будь-який вхідний біт змінений. Обчислені значення кращі, ніж 32-розрядний фіналізатор, який використовується MurmurHash , і майже такий же хороший (не зовсім), як при використанні AES . Невелика перевага полягає в тому, що одна і та ж константа використовується двічі (це зробило її трохи швидше, коли я тестував останній раз, не впевнений, чи все ще так).

Ви можете змінити процес (отримати значення вхідного сигналу від хеша) , якщо замінити 0x45d9f3bз 0x119de1f3мультиплікативної інверсії ):

unsigned int unhash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = (x >> 16) ^ x;
    return x;
}

Для 64-розрядних чисел я пропоную скористатися наступним, навіть подумав, що це може бути не найшвидшим. Цей заснований на splitmix64 , який, здається, базується на статті блогу Better Bit Mixing (змішання 13).

uint64_t hash(uint64_t x) {
    x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
    x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
    x = x ^ (x >> 31);
    return x;
}

Для Java, використання long, додати Lдо константи, замініть >>з >>>і видалити unsigned. У цьому випадку реверсування складніше:

uint64_t unhash(uint64_t x) {
    x = (x ^ (x >> 31) ^ (x >> 62)) * UINT64_C(0x319642b2d24d8ec3);
    x = (x ^ (x >> 27) ^ (x >> 54)) * UINT64_C(0x96de1b173f119089);
    x = x ^ (x >> 30) ^ (x >> 60);
    return x;
}

Оновлення: Ви також можете переглянути проект Hash Function Prospector , де вказані інші (можливо кращі) константи.


2
перші два рядки абсолютно однакові! чи є тут друкарська помилка?
Kshitij Banerjee

3
Ні, це не друк, другий рядок додатково змішує біти. Використовувати лише одне множення не так добре.
Томас Мюллер

3
Я змінив магічне число, тому що відповідно до тестового випадку я записав значення 0x45d9f3b, що забезпечує кращу плутанину і дифузію , особливо якщо один вихідний біт змінюється, то інший вихідний біт змінюється приблизно з однаковою ймовірністю (крім того, що всі вихідні біти змінюються з така ж ймовірність, якщо біт введення змінюється). Як ви оцінили, що 0x3335b369 працює для вас краще? 32-бітний інт для вас?
Томас Мюллер

3
Я шукаю хорошу хеш-функцію для 64-бітного неподписаного int до 32-бітного неподписаного int. Чи в цьому випадку вище магічне число буде однаковим? Я змістив 32 біт замість 16 біт.
алессандро

3
Я вважаю, що в цьому випадку більший фактор буде кращим, але вам потрібно буде провести кілька тестів. Або (це я і роблю) спочатку використовую, x = ((x >> 32) ^ x)а потім використовую 32-розрядні множення вище. Я не впевнений, що краще. Ви також можете подивитися 64-розрядний фіналізатор для Murmur3
Томас Мюллер

29

Залежить від способу розповсюдження ваших даних. Для простого лічильника найпростіша функція

f(i) = i

буде добре (я підозрюю оптимальне, але не можу цього довести).


3
Проблема в цьому полягає в тому, що звичайно мати великі набори цілих чисел, які поділяються загальним фактором (адреси в пам'яті, що вирівнюються за словом тощо). Тепер, якщо ваша хеш-таблиця поділяється на один і той же коефіцієнт, ви отримаєте лише половину (або 1/4, 1/8 тощо) відра, які використовуються
Rafał Dowgird

8
@Rafal: Ось чому відповідь говорить "для простого лічильника" і "Залежить від того, як розподіляються ваші дані"
erikkallen

5
Це власне реалізація методом hashCode () Sun у java.lang.Integer grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Carrion

5
@JuandeCarrion Це вводить в оману, оскільки це не хеш, який використовується. Після переходу до використання потужності двох розмірів таблиці, Java переглядає кожен хеш, повернений звідти .hashCode(), дивіться тут .
Esailija

8
Функція ідентичності є досить марною, як хеш для багатьох практичних застосувань через її розподільні властивості (або їх відсутність), якщо, звичайно, місцевість не є бажаним атрибутом
awdz9nld

12

Швидкі та хороші хеш-функції можуть складатися із швидких перестановок із меншими якостями, як-от

  • множення на нерівномірне ціле число
  • двійкові обертання
  • xorshift

Для отримання функції хешування з чудовими якостями, як це показано з PCG для генерації випадкових чисел.

Це насправді також рецепт rrxmrrxmsx_0 та хеш-шум, що використовується, свідомо чи несвідомо.

Я особисто знайшов

uint64_t xorshift(const uint64_t& n,int i){
  return n^(n>>i);
}
uint64_t hash(const uint64_t& n){
  uint64_t p = 0x5555555555555555ull; // pattern of alternating 0 and 1
  uint64_t c = 17316035218449499591ull;// random uneven integer constant; 
  return c*xorshift(p*xorshift(n,32),32);
}

щоб бути досить хорошим.

Хороша хеш-функція повинна

  1. бути біективними, щоб не втрачати інформацію, якщо можливо, і мати найменші зіткнення
  2. каскад максимально рівномірно, тобто кожен вхідний біт повинен перевертати кожен вихідний біт з вірогідністю 0,5.

Давайте спочатку розглянемо функцію ідентичності. Він задовольняє 1., але не 2.:

функція ідентичності

Біт введення n визначає вихідний біт n із співвідношенням 100% (червоний) та жодних інших, тому вони сині, даючи ідеальну червону лінію поперек.

A xorshift (n, 32) не набагато кращий, що дає півтора рядка. Все-таки задовольняючи 1., оскільки воно є зворотним при другому застосуванні.

xorshift

Множення з непідписаним цілим числом набагато краще, каскадніше сильніше і перегортаючи більше вихідних бітів з вірогідністю 0,5, що ви хочете, зеленим кольором. Він задовольняє 1. оскільки для кожного нерівномірного цілого числа існує мультиплікативна обернена.

кнут

Поєднання двох дає наступний результат, все-таки задовольняючи 1. оскільки склад двох біективних функцій дає ще одну біективну функцію.

knuth • xorshift

Друге застосування множення і xorshift призведе до наступного:

запропонований хеш

Або ви можете використовувати множення поля Galois на зразок GHash , вони стали досить швидко на сучасних процесорах і мають вищі якості за один крок.

   uint64_t const inline gfmul(const uint64_t& i,const uint64_t& j){           
     __m128i I{};I[0]^=i;                                                          
     __m128i J{};J[0]^=j;                                                          
     __m128i M{};M[0]^=0xb000000000000000ull;                                      
     __m128i X = _mm_clmulepi64_si128(I,J,0);                                      
     __m128i A = _mm_clmulepi64_si128(X,M,0);                                      
     __m128i B = _mm_clmulepi64_si128(A,M,0);                                      
     return A[0]^A[1]^B[1]^X[0]^X[1];                                              
   }

gfmul: Код видається псевдокодом, оскільки afaik ви не можете використовувати дужки з __m128i. Ще дуже цікаво. Перший рядок, як видно, говорить: "візьміть неітіалізований __m128i (I) і xor його з (параметром) i. Чи повинен я читати це як ініціалізувати я з 0 і xor з i? і виконайте не (операцію) на I?
січня

@Jan, що я хотів би це зробити __m128i I = i; //set the lower 64 bits, але це не можу, тому я використовую ^=. 0^1 = 1тому жодне не викликається. Щодо ініціалізації, з якою {}мій компілятор ніколи не скаржився, це може бути не найкращим рішенням, але те, що я хочу, - це ініціалізувати все до 0, щоб я міг зробити ^=або |=. Я думаю, що я базував цей код на цьому блозі, який також дає інверсію, дуже корисну: D
Вольфганг Брем

6

На цій сторінці перераховано кілька простих хеш-функцій, які, як правило, пристойно загалом, але будь-який простий хеш має патологічні випадки, коли він не працює добре.


6
  • 32-бітний мультиплікативний метод (дуже швидкий) див. @Rafal

    #define hash32(x) ((x)*2654435761)
    #define H_BITS 24 // Hashtable size
    #define H_SHIFT (32-H_BITS)
    unsigned hashtab[1<<H_BITS]  
    .... 
    unsigned slot = hash32(x) >> H_SHIFT
  • 32-біт і 64-біт (хороший розподіл) за адресою: MurmurHash

  • Функція хешу цілого числа

3

Тут є хороший огляд деяких алгоритмів хешу на Eternally Confuzzled . Я рекомендую одноразовий хеш Боба Дженкінса, який швидко досягає лавини і тому може бути використаний для ефективного пошуку хеш-таблиць.


4
Це хороша стаття, але вона зосереджена на хешуваннях рядкових ключів, а не цілих чисел.
Адріан Муат

Щоб було зрозуміло, хоча методи в статті будуть працювати для цілих чисел (або можуть бути адаптовані до), я припускаю, що є більш ефективні алгоритми для цілих чисел.
Адріан Муат

2

Відповідь залежить від багатьох речей, таких як:

  • Де ви маєте намір її найняти?
  • Що ви намагаєтеся зробити з хешем?
  • Вам потрібна критографічно захищена хеш-функція?

Я пропоную вам поглянути на сімейство Меркле-Дамгард хеш-функцій, таких як SHA-1 тощо


1

Я не думаю, що ми можемо сказати, що хеш-функція "хороша", не знаючи ваших даних заздалегідь! і не знаючи, що ти будеш робити з цим.

Є кращі структури даних, ніж хеш-таблиці для невідомих розмірів даних (я припускаю, що ви робите хешування для хеш-таблиці тут). Я особисто використовував би хеш-таблицю, коли знаю, що у мене є "кінцева" кількість елементів, які потребують збереження в обмеженому обсязі пам'яті. Я б спробував зробити швидкий статистичний аналіз моїх даних, побачити, як він розподіляється і т. Д., Перш ніж почати думати про свою хеш-функцію.


1

Щодо випадкових хеш-значень, деякі інженери сказали, що золоте співвідношення просте число (2654435761) є поганим вибором, і завдяки моїм результатам тестування я виявив, що це неправда; натомість 2654435761 досить добре поширює хеш-значення.

#define MCR_HashTableSize 2^10

unsigned int
Hash_UInt_GRPrimeNumber(unsigned int key)
{
  key = key*2654435761 & (MCR_HashTableSize - 1)
  return key;
}

Розмір хеш-таблиці повинен бути потужністю два.

Я написав тестову програму для оцінки багатьох хеш-функцій для цілих чисел, результати показують, що GRPrimeNumber є досить хорошим вибором.

Я намагався:

  1. total_data_entry_number / total_bucket_number = 2, 3, 4; де total_bucket_number = розмір таблиці хеш;
  2. відобразити домен хеш-значень у домен індексу ковша; тобто перетворити хеш-значення в індекс відра за допомогою Logical And Operation з (hash_table_size - 1), як показано в Hash_UInt_GRPrimeNumber ();
  3. обчислити номер зіткнення кожного відра;
  4. записувати відро, яке не було відображене на карті, тобто порожнє відро;
  5. з’ясувати максимальну кількість зіткнень усіх відра; тобто найдовша довжина ланцюга;

За результатами тестування я виявив, що Prime Ratio Prime Number завжди має менше порожніх відра або нуль порожнього відра і найкоротшу довжину ланцюга зіткнення.

Деякі хеш-функції для цілих чисел вважаються хорошими, але результати тестування показують, що коли total_data_entry / total_bucket_number = 3, найдовша довжина ланцюга перевищує 10 (максимальне число зіткнення> 10), і багато відра не відображаються (порожні відра ), що дуже погано, в порівнянні з результатом нульового порожнього відра і найдовшої довжини ланцюга 3 за допомогою Золотого коефіцієнта.

До речі, за результатами тестування я виявив, що одна версія хеш-функцій shifting-xor є досить хорошою (її поділяє mikera).

unsigned int Hash_UInt_M3(unsigned int key)
{
  key ^= (key << 13);
  key ^= (key >> 17);    
  key ^= (key << 5); 
  return key;
}

2
Але тоді чому б не змістити продукт правильно, щоб ви зберегли найбільш змішані шматочки? Це було так, як це мало працювати
harold

1
@harold, золоте співвідношення просте число ретельно вибирається, хоча, я думаю, це не змінить ніякого значення, але я перевіряю, чи набагато краще це з "найбільш змішаними бітами". Хоча моя думка полягає в тому, що "Це не вдалий вибір". Неправда, як показують результати тестування, просто захопити нижню частину бітів досить добре, і навіть краще, ніж багато хеш-функції.
Чень-ЧунгЧія

(2654435761, 4295203489) - це золоте співвідношення приматів.
Чень-ЧунгЧя

(1640565991, 2654435761) - це також золоте співвідношення прайметів.
Чень-ЧунгЧія

@harold, зсув права товару стає гіршим, навіть якщо просто змістивши праву на 1 положення (розділене на 2), воно все одно стає гіршим (хоча все-таки нульове порожнє відро, але найдовша довжина ланцюга більша); зміщуючи право на більше позицій, результат стає гіршим. Чому? Я думаю, що причина полягає в тому, що зміна продукту вправо робить більше хеш-значень, щоб не бути копром, я просто здогадуюсь, справжня причина включає теорію чисел.
Чень-ЧунгЧія

1

Я використовував splitmix64(загострений в Томаса Мюллера відповіді ) з тих пір я знайшов цю тему. Однак я нещодавно натрапив на rrxmrrxmsx_0 Pelle Evensen , який дав надзвичайно кращий статистичний розподіл, ніж оригінальний фіналізатор MurmurHash3 та його наступники ( splitmix64та інші суміші). Ось фрагмент коду в C:

#include <stdint.h>

static inline uint64_t ror64(uint64_t v, int r) {
    return (v >> r) | (v << (64 - r));
}

uint64_t rrxmrrxmsx_0(uint64_t v) {
    v ^= ror64(v, 25) ^ ror64(v, 50);
    v *= 0xA24BAED4963EE407UL;
    v ^= ror64(v, 24) ^ ror64(v, 49);
    v *= 0x9FB21C651E98DF25UL;
    return v ^ v >> 28;
}

Pelle також надає глибокий аналіз 64-бітного змішувача, що використовується на останньому етапі MurmurHash3та останніх варіантів.


2
Ця функція не є бієктивною. Для всіх v, де v = ror (v, 25), а саме всіх 0 і всіх 1, він дасть однаковий вихід у двох місцях. Для всіх значень v = ror64 (v, 24) ^ ror64 (v, 49), які є принаймні ще двома і однаковими з v = ror (v, 28), даючи ще 2 ^ 4, що становить близько 22 непотрібних зіткнень . Дві програми splitmix, напевно, такі ж хороші і настільки ж швидкі, але все-таки необоротні і не стикаються.
Вольфганг Брем
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.