Почему Java hashCode () в String использует 31 в качестве множителя?

Согласно документации Java, хэш-код для объекта String вычисляется как:

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

using int arithmetic, where s[i] is the ith character of the string, n is the length of the string, and ^ indicates exponentiation.

Почему 31 используется как множитель?

Я понимаю, что множитель должен быть относительно большим простым числом. Так почему не 29, 37 или даже 97?

Сравните также stackoverflow.com/questions/1835976/… - я думаю, что 31 - плохой выбор, если вы пишете свои собственные хэш-функции.

Hans-Peter Störr 19.05.2010 18:50

Если бы было 29, 37 или даже 97, вы бы спросили: «Почему не 31?»

user207421 13.07.2017 03:32

@EJP важно знать причину выбора «нет». если только число не является результатом трюка с черной магией.

Dushyant Sabharwal 05.09.2017 16:08

Об этом есть сообщение в блоге @ peter-lawrey здесь: vanilla-java.github.io/2018/08/12/… и здесь: vanilla-java.github.io/2018/08/15/…

Christophe Roussy 03.10.2019 12:17

@DushyantSabharwal Я хочу сказать, что у него может быть был 29, или 37, или 97, или 41, или многие другие значения, без особой практической разницы. В 1976 году мы использовали 37.

user207421 13.01.2020 07:12
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
510
5
151 391
13
Перейти к ответу Данный вопрос помечен как решенный

Ответы 13

Я не уверен, но я предполагаю, что они протестировали некоторый образец простых чисел и обнаружили, что 31 дает наилучшее распределение по некоторому образцу возможных строк.

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

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

Для большинства других процессоров потребуется отдельная инструкция сдвига и вычитания. Однако, если ваш множитель медленный, это все равно выигрыш. Современные процессоры, как правило, имеют быстрые множители, поэтому это не имеет большого значения, если 32 идет на правильную сторону.

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

Как ни странно, умножение на 31 на моем настольном компьютере на самом деле немного медленнее, чем умножение, скажем, на 92821. Я предполагаю, что компилятор пытается «оптимизировать» его для сдвига и сложения. :-)

Hans-Peter Störr 11.05.2010 10:54

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

supercat 28.03.2014 02:02

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

Tom Hawtin - tackline 28.03.2014 15:27

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

supercat 28.03.2014 20:30

Ситуация не так плоха, как та, которая возникает при использовании xor элементов, которые должны быть неупорядоченными (в отличие от использования арифметической суммы). Если у кого-то есть UnorderedPair, где A и B «равны», если (a.first.equals(b.first) && a.second.equals(b.second)) || ((a.first.equals(b.second) && a.second.equals(b.first)), использование хэша first.hashCode()+second.hashCode() приведет к потере одного бита информации, если совпадают first и second; использование first.hashCode() ^ second.hashCode() (что я видел как работающее) потеряло бы все 32 бита информации.

supercat 28.03.2014 20:34

@supercat Да, мне кажется + дает небольшое "размытие" соседних битов, которое может быть перемешано в дальнейшем. ^ не имеет этого, поэтому кажется более плохим выбором. Я считаю, что ранние протоколы использовали xor в проверочных кодах, поскольку ошибочно полагали, что реализация аппаратного обеспечения будет проще из-за стоящего фактора.

Tom Hawtin - tackline 28.03.2014 22:20

@ TomHawtin-tackline: Использование xor в аппаратных реализациях дает ряд преимуществ. Кроме того, поведение xor ортогонально поведению сложения и умножения, его использование в сочетании с другими методами может улучшить «смешивание». В любом случае важно остерегаться случаев, когда непропорциональное появление определенных шаблонов ввода (например, пар совпадающих элементов) приведет к тому, что некоторые хэш-коды будут появляться гораздо чаще, чем должны.

supercat 28.03.2014 23:14

@supercat забавный факт, хеш-код Map.Entry был исправлен спецификацией как key.hashCode() ^ value.hashCode(), несмотря на то, что это даже не неупорядоченная пара, поскольку key и value имеют совершенно разное значение. Да, это означает, что Map.of(42, 42).hashCode() или Map.of("foo", "foo", "bar", "bar").hashCode() и т. д. Предсказуемо равны нулю. Так что не используйте карты в качестве ключей для других карт ...

Holger 30.08.2019 12:12

@Holger: Что грустно в этом, так это то, что использование + вместо ^ было бы столь же быстрым, но потеряло бы только один бит хэш-кода (а иногда и не то, поскольку хэш-коды некоторых типов никогда не бывают отрицательными).

supercat 30.08.2019 17:59

@supercat и потери производительности из-за хеш-коллизий в любом случае превосходят любую экономию нескольких циклов ЦП при вычислении хэша ...

Holger 30.08.2019 19:44

@Holger: Использование сложного хеша для предотвращения потери хеш-бита из x+y, вероятно, не окупится, но хеш, возвращающий ноль для всех ключей, просто трагичен.

supercat 30.08.2019 19:59

@supercat, вы сделали правильный выбор. Даже на гипотетическом процессоре, который мог бы сэкономить цикл при использовании ^ вместо +, значительно большее количество коллизий съело бы это преимущество. Это только тогда, когда ключ и значение имеют один и тот же хэш-код, но это не слишком надумано, чтобы иметь такие сопоставления.

Holger 02.09.2019 10:02
Ответ принят как подходящий

Согласно Эффективная Java Джошуа Блоха (книга, которую нельзя рекомендовать достаточно, и которую я купил благодаря постоянным упоминаниям о stackoverflow):

The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically.

(из главы 3, пункт 9: Всегда переопределять хэш-код при переопределении равенства, стр. 48)

Ну, все простые числа нечетные, кроме 2. Просто скажи.

Kip 18.11.2008 23:15

Я не думаю, что Блох говорит, что он был выбран потому, что это было нечетное простое число, а потому, что оно было нечетным, И потому, что оно было простым (И потому, что его можно легко оптимизировать для сдвига / вычитания).

matt b 18.11.2008 23:48

31 было выбрано, потому что это нечетное простое число ??? В этом нет никакого смысла - я говорю, что 31 был выбран, потому что он давал лучшее распределение - проверьте computinglife.wordpress.com/2008/11/20/…

computinglife 20.11.2008 23:00

Я считаю, что выбор 31 весьма неудачный. Конечно, это может сэкономить несколько циклов ЦП на старых машинах, но у вас уже есть хеш-коллизии в коротких строках ascii, таких как «@ и #!» Или Ca и DB. Этого не произойдет, если вы выберете, например, 1327144003 или минимум 524287, который также допускает битовый сдвиг: 524287 * i == i << 19 - i.

Hans-Peter Störr 30.11.2009 16:43

Но зачем умножать, он просто сдвигает младшие биты от элемента влево, после того, как достаточно элементов, все ваши биты сдвигаются влево и переполняются. возможно, если бы они были недостаточными, тогда все могло бы быть лучше.

mP. 29.04.2010 03:04

@hstoerr: Я бы хотел увидеть вашу математику по этому поводу. Даже если вы правы (что вы, скорее всего, относитесь к двухзначному примеру), я думаю, что если вы посмотрите, как хеши используются в Java, на самом деле это не очень повредит, если будут коллизии в очень коротких строках. . Они не так часто используются для ключей.

Jason 08.05.2010 00:09

@Jason Смотрите мой ответ stackoverflow.com/questions/1835976/…. Я хочу сказать, что вы получите гораздо меньше столкновений, если используете большее простое число, и в наши дни ничего не потеряете. Проблема усугубляется, если вы используете неанглийские языки с обычными символами, отличными от ascii. А 31 послужил плохим примером для многих программистов при написании собственных хэш-функций.

Hans-Peter Störr 12.05.2010 11:42

Действительно ли оптимизация сейчас помогает с учетом арифметических устройств в современных процессорах?

Richard Corfield 22.04.2014 20:55

@hstoerr Полностью согласен с вашим. Использование 31 было ужасной идеей. Не должно быть конфликтов для строк длиной два и не должно быть конфликтов для строк ASCII длины четыре, но их много. Повторное использование 31 повсюду еще хуже, см., Например, эта моя крошечная тирада.

maaartinus 31.05.2014 17:56

@RichardCorfield Я не уверен, имеет ли это смысл, но компилятор все еще использует его (см. Комментарии к этот ответ).

maaartinus 31.05.2014 18:00

Каждый изучающий математику должен знать, почему он простой - он образует группу тогда и только тогда, когда он совпадает с размером группы.

J-16 SDiZ 27.11.2014 13:55

Я не уверен, почему это было принято в качестве ответа. Я имею в виду, что это просто скопируйте пасту из известной книги !! Выпускник без математики и информатики вроде меня искал ответ с точки зрения непрофессионала. Прошу прощения, но ответ для меня слишком интеллектуален. Мне нужно больше искать в Google.

Raj 05.08.2017 16:31

Как происходит «умножение на четное число с переполнением»? Переполнение может происходить даже с нечетными числами, правильно?

Frank Q. 04.10.2017 04:26

@FrankQ. Проблема не в переполнении: это, как вы говорите, неизбежно. Проблема в том, что умножение на четное число гарантирует, что меньшее количество битов будет содержать «изменяющуюся» информацию - младший бит становится нулевым. Всегда ноль. Вы потеряли немного «изменчивости». Результатом является более плохое распределение возможных значений хеш-функции.

ToolmakerSteve 01.03.2018 05:43

Goodrich и Tamassia вычислили из более чем 50 000 английских слов (сформированных как объединение списков слов, представленных в двух вариантах Unix), что использование констант 31, 33, 37, 39 и 41 вызовет менее 7 коллизий в каждом случае. Это может быть причиной того, что многие реализации Java выбирают такие константы.

См. Раздел 9.2 Хеш-таблицы (стр. 522) в Структуры данных и алгоритмы в Java.

Обратите внимание, однако, что вы можете получить НАМНОГО больше коллизий, если используете любую международную кодировку с общими символами за пределами диапазона ASCII. По крайней мере, проверял на 31 и немецком. Так что думаю выбор 31 сломан.

Hans-Peter Störr 11.05.2010 10:58

При умножении биты сдвигаются влево. Это использует больше доступного пространства для хэш-кодов, уменьшая коллизии.

Если не использовать степень двойки, то младшие правые биты также заполняются для смешивания со следующей частью данных, попадающих в хэш.

Выражение n * 31 эквивалентно (n << 5) - n.

Блох не совсем вникает в это, но я всегда слышал / верил в обоснование того, что это базовая алгебра. Хеши сводятся к операциям умножения и модуля, а это означает, что вы никогда не захотите использовать числа с общими множителями, если можете. Другими словами, относительно простые числа обеспечивают равномерное распределение ответов.

Числа, которые составляют с помощью хеша, обычно:

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

На самом деле вы можете контролировать только несколько из этих значений, так что требуется небольшая дополнительная осторожность.

На самом деле, 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 байтов для перемещения + сдвига + вычитания для умножения на 31. Одно из возможных предупреждений заключается в том, что Используемые здесь 3-аргументные инструкции LEA стали медленнее в архитектуре Intel Sandy bridge с увеличенной задержкой на 3 цикла.

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

@Mainguy На самом деле это синтаксис АЛГОЛА, который довольно часто используется в псевдокоде.

ApproachingDarknessFish 27.12.2013 07:53

но в сборке ARM умножение на 31 может быть выполнено в одной инструкции

phuclv 21.04.2015 11:26

@Mainguy Что означает: = в псевдокоде?

phuclv 21.04.2015 11:52

В TPOP (1999) можно прочитать о ранней версии Java (стр.57): «... Проблема была решена заменой хэша на один эквивалентный тому, который мы показали (с множителем 37) ...»

miku 15.01.2017 03:36

Нил Коффи объясняет, почему 31 используется под Сглаживание предвзятости.

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

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

Of the remaining four, I'd probably select P(31), as it's the cheapest to calculate on a RISC machine (because 31 is the difference of two powers of two). P(33) is similarly cheap to calculate, but it's performance is marginally worse, and 33 is composite, which makes me a bit nervous.

Таким образом, рассуждение было не таким рациональным, как предполагают многие из приведенных здесь ответов. Но мы все умеем придумывать рациональные причины после интуитивных решений (и даже Блох может быть к этому склонен).

Из JDK-4045622, где Джошуа Блох описывает причины, по которым была выбрана именно эта (новая) реализация String.hashCode().

The table below summarizes the performance of the various hash functions described above, for three data sets:

1) All of the words and phrases with entries in Merriam-Webster's 2nd Int'l Unabridged Dictionary (311,141 strings, avg length 10 chars).

2) All of the strings in /bin/, /usr/bin/, /usr/lib/, /usr/ucb/ and /usr/openwin/bin/* (66,304 strings, avg length 21 characters).

3) A list of URLs gathered by a web-crawler that ran for several hours last night (28,372 strings, avg length 49 characters).

The performance metric shown in the table is the "average chain size" over all elements in the hash table (i.e., the expected value of the number of key compares to look up an element).

                          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

Looking at this table, it's clear that all of the functions except for the current Java function and the two broken versions of Weinberger's function offer excellent, nearly indistinguishable performance. I strongly conjecture that this performance is essentially the "theoretical ideal", which is what you'd get if you used a true random number generator in place of a hash function.

I'd rule out the WAIS function as its specification contains pages of random numbers, and its performance is no better than any of the far simpler functions. Any of the remaining six functions seem like excellent choices, but we have to pick one. I suppose I'd rule out Vo's variant and Weinberger's function because of their added complexity, albeit minor. Of the remaining four, I'd probably select P(31), as it's the cheapest to calculate on a RISC machine (because 31 is the difference of two powers of two). P(33) is similarly cheap to calculate, but it's performance is marginally worse, and 33 is composite, which makes me a bit nervous.

Josh

В последней версии 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, затем вычтите само себя, поэтому нужны дешевые ресурсы.

Это потому, что 31 имеет приятное свойство - его умножение можно заменить побитовым сдвигом, который быстрее стандартного умножения:

31 * i == (i << 5) - i

Большое ожидание от хэш-функций заключается в том, что равномерная случайность их результата выдерживает такую ​​операцию, как hash(x) % N, где N - произвольное число (и во многих случаях степень двойки), одна из причин заключается в том, что такие операции обычно используются в хеш-таблицах для определения слоты. Использование множителей простых чисел при вычислении хеша снижает вероятность того, что ваш множитель и N делят делители, что сделает результат операции менее случайным.

Другие указали на то замечательное свойство, что умножение на 31 может быть выполнено умножением и вычитанием. Я просто хочу указать, что для таких простых чисел существует математический термин: Мерсенн Прайм

Все простые числа Мерсенна на единицу меньше степени двойки, поэтому мы можем записать их как:

p = 2^n - 1

Умножая x на p:

x * p = x * (2^n - 1) = x * 2^n - x = (x << n) - x

Сдвиги (SAL / SHL) и вычитания (SUB) обычно выполняются быстрее, чем умножение (MUL) на многих машинах. См. таблицы инструкций от Agner Fog

Вот почему GCC, кажется, оптимизирует умножение на простые числа Мерсена, заменяя их сдвигами и подстановками, глянь сюда.

Однако, на мой взгляд, такое маленькое простое число - плохой выбор для хеш-функции. При относительно хорошей хеш-функции можно ожидать случайности в старших битах хеш-функции. Однако с хеш-функцией Java почти нет случайности в старших битах с более короткими строками (и все еще очень сомнительной случайности в младших битах). Это затрудняет создание эффективных хеш-таблиц. См. этот приятный трюк, который вы не смогли бы сделать с хеш-функцией Java.

В некоторых ответах упоминается, что, по их мнению, хорошо, что 31 помещается в байт. На самом деле это бесполезно, поскольку:

(1) Мы выполняем сдвиги вместо умножения, поэтому размер множителя не имеет значения.

(2) Насколько мне известно, нет специальной инструкции x86 для умножения 8-байтового значения на 1-байтовое значение, поэтому вам все равно пришлось бы преобразовать «31» в 8-байтовое значение, даже если вы умножали. Смотрите здесь, вы умножаете целые 64-битные регистры.

(А 127 на самом деле является самым большим простым числом Мерсенна, которое может уместиться в байте.)

Увеличивает ли меньшее значение случайность в средних и младших битах? Возможно, но это также, кажется, значительно увеличивает количество возможных столкновений :).

Можно перечислить много разных проблем, но обычно они сводятся к двум основным принципам, которые не выполняются должным образом: Путаница и Распространение

Но быстро ли это? Наверное, потому что мало что делает. Однако, если здесь в центре внимания производительность, один символ в цикле будет довольно неэффективным. Почему бы не делать 4 символа за раз (8 байтов) на итерацию цикла для более длинных строк, нравится? Что ж, это было бы сложно сделать с текущим определением хэша, где вам нужно умножать каждый символ индивидуально (пожалуйста, скажите мне, есть ли небольшой хак, чтобы решить эту проблему: D).

Другие вопросы по теме