Чому хеш-код () Java у String використовує 31 як множник?


480

Відповідно до документації Java, хеш-код для Stringоб'єкта обчислюється як:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

з використанням intарифметичних операцій, де s[i]це я й символ рядка, nдовжина рядка, і ^вказує , зведення в ступінь.

Чому 31 використовується як множник?

Я розумію, що множник повинен бути порівняно великим простим числом. То чому б не 29, 37, а то й 97?


1
Порівняйте також stackoverflow.com/questions/1835976/… - Я думаю, що 31 - це поганий вибір, якщо ви пишете власні функції hashCode.
Ганс-Пітер Стрер

6
Якби це було 29, 37 чи навіть 97, ви б запитали "чому б не 31?"
Маркіз Лорн

2
@EJP Важливо знати причину вибору "ні". якщо тільки число не є результатом чорного магічного трюку.
Dushyant Sabharwal

Тут розміщена публікація в блозі @ peter-lawrey: vanilla-java.github.io/2018/08/12/… і тут: vanilla-java.github.io/2018/08/15/…
Крістоф Руссі

@DushyantSabharwal Моя думка полягає в тому, що це могло бути 29 або 37, 97, або 41, або багато інших значень, не роблячи особливих різниць. Ми використовували 37 в 1976 році
маркіз Лорн

Відповіді:


405

Відповідно до Ефективної Java Джошуа Блоха (книги, яку недостатньо рекомендувати, і яку я придбав завдяки постійним згадкам про stackoverflow):

Значення 31 було обрано тому, що це непарний простий показник. Якби воно було парним, і множення переповнилося, інформація втрачалася б, оскільки множення на 2 еквівалентно зміщенню. Перевага використання прайме менш зрозуміла, але традиційна. Приємне властивість 31 є те , що множення може бути замінено на зрушення і відніманням для кращої продуктивності: 31 * i == (i << 5) - i. Сучасні віртуальні машини роблять подібну оптимізацію автоматично.

.


346
Добре, що всі прайми є непарними, крім 2. Просто скажіть.
Кіп

38
Я не думаю, що Блох каже, що його вибрали тому, що це був дивний прайм, а тому, що це було дивно І тому, що це було простим (І тому, що його можна легко оптимізувати в зсув / віднімання).
мат b

50
31 було обрано coz це дивний прем'єр ??? Це не має сенсу - я кажу, що 31 був обраний тому, що він дав найкращий розподіл - перевірте computinglife.wordpress.com/2008/11/20/…
computinglife

65
Я думаю, що вибір 31 є досить невдалим. Звичайно, це може зберегти кілька циклів процесора на старих машинах, але у вас є хеш-зіткнення вже на коротких рядках ascii, таких як "@ і #! Або Ca і DB. Це не відбудеться, якщо ви виберете, наприклад, 1327144003 або на принаймні 524287, що також дозволяє змінити біт: 524287 * i == i << 19 - i.
Ганс-Пітер Стрерр

15
@Jason Дивіться мою відповідь stackoverflow.com/questions/1835976 / ... . Моя думка: ви отримуєте набагато менше зіткнень, якщо використовуєте більший прайм, і нічого цього не втрачаєте. Проблема ще гірша, якщо ви використовуєте неанглійські мови із загальними символами, які не мають права. І 31 послужив поганим прикладом для багатьох програмістів при написанні власних функцій hashCode.
Ганс-Пітер Стрер

80

Як зазначають Гудрич і Тамасія , якщо ви візьмете понад 50 000 англійських слів (сформованих як об'єднання списків слів, передбачених у двох варіантах Unix), використання констант 31, 33, 37, 39 і 41 призведе до менш ніж 7 зіткнень у кожному випадку. Знаючи це, не дивно, що багато реалізацій Java вибирають одну з цих констант.

Випадково я опинився в середині читання розділу "поліномальні хеш-коди", коли побачив це запитання.

EDIT: тут посилання на ~ 10mb PDF-книжку, на яку я згадуюсь вище. Дивіться розділ 10.2 Таблиці хеш-структур (стор. 413) структур даних та алгоритмів Java


6
Зауважте, однак, що у вас можуть виникнути ВІДНОЛІШНІ зіткнення, якщо ви використовуєте будь-який міжнародний набір із загальними символами поза діапазоном ASCII. Принаймні, я перевірив це на 31 і німецьку. Тому я думаю, що вибір 31 порушений.
Ганс-Пітер Штерр

1
@jJack, Посилання, надане у вашій відповіді, порушено.
СК Венкат

Обидві ланки у цій відповіді розірвані. Також аргумент у першому абзаці є свого роду неповним; як інші непарні числа порівнюються з п'ятьма, які ви перераховуєте на цьому еталоні?
Марк

58

На (переважно) старих процесорах множення на 31 може бути порівняно дешевим. Наприклад, для ARM це лише одна інструкція:

RSB       r1, r0, r0, ASL #5    ; r1 := - r0 + (r0<<5)

Більшість інших процесорів потребують окремої інструкції зсуву та віднімання. Однак якщо ваш множник повільний, це все-таки виграш. Сучасні процесори, як правило, мають швидкі мультиплікатори, тому це не має великої різниці, доки 32 йдуть на правильну сторону.

Це не чудовий алгоритм хешування, але він досить хороший і кращий, ніж код 1.0 (і набагато краще, ніж 1.0 специфікація!).


7
Досить смішно, множення на 31 на моєму настільному верстаті насправді трохи повільніше, ніж множення, скажімо, з 92821. Я думаю, компілятор намагається "оптимізувати" його в зсув і додати також. :-)
Ганс-Пітер Стрерр

1
Я не думаю, що я ніколи не використовував ARM, який не був однаково швидким із усіма значеннями в діапазоні +/- 255. Використання потужності 2 мінус одна призводить до того, що зміна відповідності двом значенням змінює хеш-код на два. Значення -31 було б краще, і я думаю, що щось на зразок -83 (64 + 16 + 2 + 1), можливо, було б ще краще (блендерні шматочки дещо краще).
supercat

@supercat Не переконаний у мінусі. Здається, ви б рухалися назад до нулів. / String.hashCodeпередує StrongARM, який, IIRC, ввів 8-бітний множник і, можливо, збільшився до двох циклів для комбінованої арифметичної / логічної операцій зі зміною.
Том Хотін - тайклін

1
@ TomHawtin-tackline: Використовуючи 31, хеш із чотирьох значень буде 29791 * a + 961 * b + 31 * c + d; використовуючи -31, було б -29791 * a + 961 * b - 31 * c + d. Я не думаю, що різниця була б суттєвою, якщо чотири елементи є незалежними, але якщо пари сусідніх предметів збігаються, отриманий хеш-код буде внеском усіх непарних елементів, плюс кілька кратних 32 (від парних). Для рядків це може не мати великого значення, але якщо хтось пише метод загального призначення для хешування агрегацій, ситуація, коли суміжні елементи збігаються, буде непропорційно поширеною.
supercat

3
@supercat забавний факт, хеш код Map.Entryбув зафіксований специфікацією бути , key.hashCode() ^ value.hashCode()незважаючи на це навіть не невпорядкована пара, так keyі valueмають зовсім інше значення. Так, це означає, що Map.of(42, 42).hashCode()або Map.of("foo", "foo", "bar", "bar").hashCode()тощо, передбачувано дорівнює нулю. Тому не використовуйте карти як ключі для інших карт…
Holger

33

Помноживши, біти зміщуються вліво. Тут використовується більше доступного простору хеш-кодів, що зменшує зіткнення.

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

Вираз n * 31рівносильний (n << 5) - n.


29

Ви можете прочитати оригінальні міркування Блоха в розділі "Коментарі" на http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622 . Він досліджував виконання різних хеш-функцій стосовно отриманого "середнього розміру ланцюга" в хеш-таблиці. P(31)була однією з найпоширеніших функцій того часу, яку він виявив у книзі K&R (але навіть Керніган та Річі не могли згадати, звідки вона походить). Врешті-решт, він в основному повинен був вибрати один, і тому він взяв, P(31)оскільки, здавалося, він працює досить добре. Незважаючи на те, що P(33)насправді було не гірше, і множення на 33 однаково швидко обчислити (лише зміна на 5 і додавання), він вибрав 31, оскільки 33 не є простим:

З решти чотирьох я, мабуть, обрав би P (31), оскільки це найдешевший підрахунок на машині RISC (адже 31 - різниця двох потужностей двох). P (33) аналогічно дешевий для обчислення, але його продуктивність незначно гірша, а 33 - композитний, що робить мене трохи нервовим.

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


2
Ретельне дослідження та неупереджена відповідь!
Вішал К

22

Насправді 37 працювали б досить добре! z: = 37 * x можна обчислити як y := x + 8 * x; z := x + 4 * y. Обидва кроки відповідають одній інструкції LEA x86, тому це надзвичайно швидко.

Насправді, множення з ще більшим простим рівнем 73 можна було зробити з однаковою швидкістю, встановивши y := x + 8 * x; z := x + 8 * y.

Використання 73 або 37 (замість 31) може бути кращим, оскільки це призводить до більш щільного коду : Дві інструкції LEA беруть лише 6 байт проти 7 байтів для переміщення + shift + віднімання для множення на 31. Один з можливих застережень: 3-аргументаційні інструкції LEA, використані тут, стали повільнішими в архітектурі Sandy Bridge від Intel, зі збільшенням затримки на 3 цикли.

Більше того, 73 - це улюблене число Шелдона Купера.


5
Ви паскальний програміст чи щось таке? що з: = речі?
Mainguy

11
@Mainguy Насправді синтаксис ALGOL і використовується досить часто в псевдо-коді.
Наближення

4
але у збірці ARM множення на 31 можна зробити за однією інструкцією
phuclv


У TPOP (1999) можна прочитати про ранню Java (с.57): "... Проблема була вирішена шляхом заміни хеша на один еквівалент того, який ми показали (з множником 37 ) ..."
miku

19

Ніл Коффі пояснює, чому 31 використовується при прасуванні зміщення .

В основному використання 31 дає більш рівномірний розподіл ймовірностей для хеш-функції.


12

З JDK-4045622 , де Джошуа Блох описує причини, по яких String.hashCode()було обрано саме цю (нову) реалізацію

У таблиці нижче наведено ефективність різних хеш-функцій, описаних вище, для трьох наборів даних:

1) Усі слова та фрази із записами у 2-му міжнародному слові Мерріам-Вебстер (311,141 рядки, середня довжина 10 символів).

2) Усі рядки в / bin / , / usr / bin / , / usr / lib / , / usr / ucb / та / usr / openwin / bin / * (66,304 рядків, середня довжина 21 символу).

3) Список URL-адрес, зібраних веб-сканером, який працював упродовж декількох годин минулої ночі (28 372 рядка, середня довжина 49 символів).

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

                          Webster's   Code Strings    URLs
                          ---------   ------------    ----
Current Java Fn.          1.2509      1.2738          13.2560
P(37)    [Java]           1.2508      1.2481          1.2454
P(65599) [Aho et al]      1.2490      1.2510          1.2450
P(31)    [K+R]            1.2500      1.2488          1.2425
P(33)    [Torek]          1.2500      1.2500          1.2453
Vo's Fn                   1.2487      1.2471          1.2462
WAIS Fn                   1.2497      1.2519          1.2452
Weinberger's Fn(MatPak)   6.5169      7.2142          30.6864
Weinberger's Fn(24)       1.3222      1.2791          1.9732
Weinberger's Fn(28)       1.2530      1.2506          1.2439

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

Я би виключав функцію WAIS, оскільки її специфікація містить сторінки випадкових чисел, а її продуктивність не краща за будь-яку з набагато простіших функцій. Будь-яка з решти шести функцій здається чудовим вибором, але ми повинні вибрати одну. Я вважаю, що я би виключив варіант Во та функцію Вайнбергера через їх додаткову складність, хоч і незначну. З решти чотирьох я, мабуть, обрав би P (31), оскільки це найдешевший підрахунок на машині RISC (адже 31 - різниця двох потужностей двох). P (33) аналогічно дешевий для обчислення, але його продуктивність незначно гірша, а 33 - композитний, що робить мене трохи нервовим.

Джош


5

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

Номери, що складаються за допомогою хеша, зазвичай:

  • модуль типу даних, який ви вводите в нього (2 ^ 32 або 2 ^ 64)
  • модуль кількості відра у вашому хеш-файлі (варіюється. У java раніше був простим, зараз 2 ^ n)
  • помножити або змістити на магічне число у вашій функції змішування
  • Вхідне значення

Ви дійсно отримаєте можливість контролювати лише пару цих значень, тому трохи додаткової обережності.


4

В останній версії JDK досі використовується 31. https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/lang/String.html#hashCode ()

Призначення хеш-рядка:

  • унікальний (Дозвольте побачити оператора ^в документі обчислення хеш-коду, він допоможе унікальним)
  • дешева вартість розрахунку

31 - це максимальне значення, можна ввести 8-розрядний (= 1 байт) регістр, найбільше просте число можна помістити в 1 байт-регістр, непарне число.

Помножте 31 - це << 5, тоді відніміть себе, тому потрібні дешеві ресурси.


3

Я не впевнений, але я б припустив, що вони протестували деяку вибірку простих чисел і виявили, що 31 дає найкращий розподіл по деякій вибірці можливих рядків.


1

Це тому, що 31 має хорошу властивість - його множення може бути замінено побітовим зсувом, який швидше, ніж стандартне множення:

31 * i == (i << 5) - i
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.