Какое простое число является разумным для вычисления хэш-кода?

В Eclipse 3.5 есть очень хорошая функция для создания функций Java hashCode (). Например, он будет генерировать (немного укороченный :)

class HashTest {
    int i;
    int j;        
    public int hashCode() {
        final int prime = 31;
        int result = prime + i;
        result = prime * result + j;
        return result;
    }
}

(Если в классе больше атрибутов, result = prime * result + attribute.hashCode(); повторяется для каждого дополнительного атрибута. Для целых значений .hashCode () можно опустить.)

Это кажется прекрасным, но для выбора 31 для прайма. Вероятно, это взято из реализации hashCode Java String, который использовался по соображениям производительности, которые давно исчезли после введения аппаратных умножителей. Здесь у вас много конфликтов хэш-кода для небольших значений i и j: например, (0,0) и (-1,31) имеют одно и то же значение. Я думаю, что это Плохая Вещь (TM), поскольку небольшие значения встречаются часто. Для String.hashCode вы также найдете много коротких строк с тем же хэш-кодом, например «Ca» и «DB». Если вы возьмете большое простое число, эта проблема исчезнет, ​​если вы выберете правильное простое число.

Итак, мой вопрос: какое простое решение выбрать? Какие критерии вы применяете, чтобы его найти?

Это общий вопрос, поэтому я не хочу давать диапазон для i и j. Но я полагаю, что в большинстве приложений относительно небольшие значения встречаются чаще, чем большие. (Если у вас большие значения, выбор простого числа, вероятно, не имеет значения.) Это может не иметь большого значения, но лучший выбор - это простой и очевидный способ улучшить это - так почему бы не сделать это? Commons lang HashCodeBuilder также предлагает любопытно маленькие значения.

(Уточнение: это не дубликата Почему Java hashCode () в String использует 31 в качестве множителя?, поскольку мой вопрос не касается истории 31 в JDK, а о том, что было бы лучше в новом коде, использующем тот же базовый шаблон. Ни один из приведенных здесь ответов не пытается ответить на этот вопрос.)


person Hans-Peter Störr    schedule 02.12.2009    source источник
comment
31 по-прежнему хорош, поскольку не обязательно требует загрузки константы. На процессоре ARM (по крайней мере, тот, который используется примерно 99,9997% мобильных телефонов) *31 можно сделать с помощью одной инструкции. На самом деле, достаточно любого нечетного числа, простого или нет.   -  person Tom Hawtin - tackline    schedule 03.12.2009
comment
Я думал о программах для настольных компьютеров, где неважно, выберете ли вы 31 или 1327144003. Любопытно, что на моей машине умножение на 31 на самом деле немного медленнее - вероятно, оптимизация пошла не так. 8-)   -  person Hans-Peter Störr    schedule 03.12.2009
comment
Простые числа формы p = (2^n-1) поддаются оптимизации x * p = (p << n) - p, которую обычно делает компилятор. Из Джошуа Блоха, Эффективная Java, глава 3, пункт 9. SO question stackoverflow.com/questions/299304/   -  person corsiKa    schedule 16.02.2011
comment
и умножение на целое число ‹128 дает дополнительный импульс в jvm .. 2^n-1, prime, smallish .. это дает 31.   -  person J-16 SDiZ    schedule 27.11.2014
comment
@corsiKa Как я уже сказал, для нынешних настольных компьютеров это больше не похоже на оптимизацию - время то же самое. Хуже того: на моей машине умножение на 31 было немного медленнее - возможно, JVM пыталась оптимизировать его, вычисляя x ‹< 5 - x, а это на самом деле медленнее, чем использование аппаратного умножителя.   -  person Hans-Peter Störr    schedule 28.11.2014
comment
@ Dr.Hans-PeterStörr На i86 есть разница, поскольку есть режим для однобайтового непосредственного операнда. Вы получаете более короткую инструкцию, и в тесте, который я написал много лет назад, она была немного быстрее.   -  person maaartinus    schedule 04.06.2017
comment
@MarkRotteveel Обратите внимание, что это сильно отличается от [Почему Java hashCode () в String использует 31 в качестве множителя?] [1], поскольку речь идет не об истории 31, а о том, что было бы лучше вместо использования 31, без использования дополнительных библиотек или совершенно иных методов вычисления хешей. Ни один из ответов не касается этого. [1]: stackoverflow.com/questions/299304/   -  person Hans-Peter Störr    schedule 05.09.2017


Ответы (6)


Я рекомендую использовать 92821. Вот почему.

Чтобы дать содержательный ответ на этот вопрос, вы должны кое-что знать о возможных значениях i и j. Единственное, о чем я могу думать в целом, это то, что во многих случаях маленькие значения будут более распространенными, чем большие значения. (Вероятность появления 15 в качестве значения в вашей программе намного выше, чем, скажем, 438281923.) Таким образом, кажется хорошей идеей сделать наименьшее столкновение хэш-кода как можно большим, выбрав подходящее простое число. Для 31 это довольно плохо - уже для i=-1 и j=31 у вас такое же хеш-значение, как для i=0 и j=0.

Поскольку это интересно, я написал небольшую программу, которая просматривала весь диапазон int в поисках лучшего простого числа в этом смысле. То есть для каждого простого числа я искал минимальное значение Math.abs(i) + Math.abs(j) по всем значениям i,j, которые имеют тот же хэш-код, что и 0,0, а затем взял простое число, где это минимальное значение максимально велико.

Барабан: лучшее простое число в этом смысле - 486187739 (с наименьшим столкновением i=-25486, j=67194). Примерно так же хорош и намного проще запомнить 92821 с наименьшим столкновением i=-46272 and j=46016.

Если вы придадите «маленькому» другое значение и хотите, чтобы минимум Math.sqrt(i*i+j*j) для столкновения был как можно большим, результаты будут немного другими: лучшим будет 1322837333 с i=-6815 and j=70091, но мой любимый 92821 (наименьшее столкновение -46272,46016) снова почти так же хорошо, как лучшее соотношение цены и качества.

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

person Hans-Peter Störr    schedule 12.05.2010
comment
Вы ищете волшебное число для идеального хеша, во всяком случае, почти идеального. Мне было бы больше интересно увидеть решение для произвольных входных данных вплоть до размера хэша (например, 4 2-байтовых значения в 8-байтовом хэш-коде), чем этот частный случай простой транспозиции. - person Jason; 13.05.2010
comment
8-байтовый хэш-код? По крайней мере, в Java это 4 байта. В любом случае: вы можете просто продолжить схему, которая используется при генерации хэш-кода eclipse: result = prime * result + i; результат = простое число * результат + j; и так далее. Для этого 92821, вероятно, будет хорошим выбором в качестве основного - по крайней мере, намного лучше, чем 31 по умолчанию для eclipse. - person Hans-Peter Störr; 18.05.2010
comment
Мало того, что небольшая константа используется неправильно, ее повторное использование также неверно, поскольку вы получаете коллизии, подобные newArrayList("a", "bc").hashCode() == newArrayList("ab", "c").hashCode() (мой пример может не работать, но что-то подобное работает). - person maaartinus; 04.06.2017
comment
@maaartinus Вы правы в том, что существует много гораздо лучших алгоритмов хеширования. Я просто пытался указать на простое, но стоящее улучшение часто используемого простого алгоритма. Если вам нужны действительно хорошие свойства, есть библиотеки для них, которые намного лучше, но их часто бывает слишком много. - person Hans-Peter Störr; 06.06.2017
comment
@ Dr.Hans-PeterStörr Что я имею в виду: даже если мы назовем n хорошей константой, использование ее повсюду не может быть правильным, поскольку она систематически порождает ненужные конфликты. ИМХО лучше использовать одну константу для строк и другую для списков. - person maaartinus; 06.06.2017
comment
@maaartinus - Хм, интересная гипотеза. Тем не менее, мое чутье таково, что характер распределения для разных типов настолько различается, что проблема, о которой вы говорите, не является доминирующей. Меня больше беспокоит то, что, как я думал, вы подняли: сомнительно ли повторять использование одного и того же простого числа для каждого дополнительного входного значения? Я думаю, что нужно использовать разные простые числа на каждом шаге умножения-сложения. - person ToolmakerSteve; 01.03.2018
comment
@ToolmakerSteve Primes на самом деле ничем не лучше других нечетных чисел (если вы не используете и маленькие ключи, и хеш-таблицу простого размера, чего вы, скорее всего, не будете). Если вы хотите использовать больше констант, вы также можете заменить шаг multiply-add на сумму кратных и получите почти трехкратное ускорение в качестве бонуса. Если бы мне пришлось написать генератор, я бы получил множители как из имени класса, так и из имени поля (или индекса). Проблема в том, как определить хороший множитель. Он должен быть необычным, большим и иметь забавные узоры. ;) - person maaartinus; 01.03.2018
comment
@maaartinus - меня бы удивило, если бы использование разных констант для разных классов дало значительную (›10%) выгоду, за исключением чрезвычайно редких ситуаций. Но если вы собирались это сделать, не пытайтесь найти хороший множитель: просто составьте таблицу из 256 из них и выполните XOR байтов хэша имени класса, чтобы получить индекс. - person ToolmakerSteve; 01.03.2018
comment
@ToolmakerSteve Я тоже сомневаюсь, 10% выполнимо. Для приложения это невероятно того стоит. Если бы мы могли перепроектировать все хеширование Java, то можно было бы достичь 10% (избегая глупых коллизий, таких как hashCode, равный нулю для любого нового Map.Entry с равным ключом и значением и т. Д.), В то время как даже 0,1%, вероятно, было бы достойным улучшением. - person maaartinus; 02.03.2018
comment
@ToolmakerSteve Идея использования нескольких простых чисел, безусловно, хороша и ничего не стоит, хотя я немного не понимаю, какие из них хороши. Но, возможно, подойдет любой, имеющий более 16 бит. Кстати, раз уж эта тема вам небезразлична: не могли бы вы проголосовать за повторное открытие вопроса? Кто-то ошибочно пометил это как дубликат, без нужды предотвращая появление новых интересных ответов, если они есть. - person Hans-Peter Störr; 02.03.2018

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

person Pascal Cuoq    schedule 02.12.2009
comment
Да, арифметика по модулю делает задачу сложной и интересной. Думаю, напишу небольшую программу для поиска хорошего решения. :-) - person Hans-Peter Störr; 03.12.2009

Коллизии могут не быть такой большой проблемой ... Основная цель хеширования - избежать использования равенства для сравнений 1: 1. Если у вас есть реализация, в которой equals «обычно» чрезвычайно дешево для объектов, которые столкнулись с хешами, то это не проблема (вообще).

В конце концов, лучший способ хеширования зависит от того, что вы сравниваете. В случае пары int (как в вашем примере) может быть достаточно использования базовых побитовых операторов (например, использования & или ^).

person Romain    schedule 02.12.2009
comment
Конечно, это не имеет большого значения, но изменение прайма - очевидный и простой способ улучшить ситуацию. Так почему бы не сделать это? - person Hans-Peter Störr; 03.12.2009
comment
Согласованный. В первую очередь я хотел сделать небольшой акцент на том факте, что использование простых чисел не является единственным способом решения задач, поскольку вопрос, в конечном счете, имеет очень общий характер. - person Romain; 03.12.2009
comment
Кстати: использование && было бы очень плохим, поскольку это имеет тенденцию уменьшать количество бит, устанавливаемых после каждого шага. Использование ^ лучше, но, как кто-то заметил, использование i ^ j будет означать, что результат равен 0, если они равны, что интуитивно также является довольно распространенным случаем. - person Hans-Peter Störr; 26.08.2020

Вам нужно определить свой диапазон для i и j. Вы можете использовать простое число для обоих.

public int hashCode() {
   http://primes.utm.edu/curios/ ;)
   return 97654321 * i ^ 12356789 * j;
}
person Peter Lawrey    schedule 02.12.2009

Я бы выбрал 7243. Достаточно большой, чтобы избежать столкновений с маленькими числами. Не переполняется быстро на маленькие числа.

person Erich Kitzmueller    schedule 02.12.2009
comment
Я использую первые 1000 простых чисел как удобный источник небольших простых чисел primes.utm.edu/ списки / small / 1000.txt - person Steve Kuo; 03.12.2009
comment
Я не думаю, что переполнение имеет значение - если простое число достаточно велико, результат будет большим даже после переполнения. Я думал о чем-то вроде 1327144003. - person Hans-Peter Störr; 03.12.2009

Я просто хочу указать, что хэш-код не имеет ничего общего с простыми числами. В реализации JDK

for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }

Я обнаружил, что если вы замените 31 на 27, результат будет очень похож.

person neoedmund    schedule 15.10.2016
comment
Простые числа - это простой способ гарантировать, что каждый хэш-код действительно встречается, так что вы не тратите ни одного бита, если целочисленное пространство для их широкого распределения. Я не совсем уверен, есть ли другие преимущества. Но вы правы, что 27, вероятно, тоже так делают. Так что он примерно так же плох, как и исходный вариант 31 - вы также получите очень небольшие коллизии хеш-кода. ;-) - person Hans-Peter Störr; 18.10.2016
comment
@ Dr.Hans-PeterStörr Для хеш-таблиц размеров, которые являются степенями двойки, все, что вам нужно, - это нечетный множитель, простой или нет. Множители на простые числа важны для таблиц простого размера, поскольку они не имеют общего множителя (если только вам не повезло использовать одно и то же простое число: D). AFAIK единственное использование таблицы простого размера в JDK находится в String#intern. - person maaartinus; 04.06.2017
comment
@maaartinus Для чего нужен нечетный множитель? Как я уже говорил, коллизии хэш-кода плохо сказываются на производительности, и маленькие множители создают больше коллизий хэш-кода, поскольку маленькие значения для атрибутов более вероятны, чем большие значения. - person Hans-Peter Störr; 05.06.2017
comment
@ Dr.Hans-PeterStörr. Нечетный множитель необходим, чтобы не потерять информацию (худшие множители - те, которые заканчиваются множеством нулей в двоичной системе). Очевидно, что потеря информации - это плохо, и ее легко избежать. +++ Мы согласны с тем, что маленькие множители - это тоже плохо. +++ Моя точка зрения была на первичность. Множитель типа m = 101*103*107*109 - это катастрофа для хеш-таблицы размера 103 (но никто не использует такие размеры). Для таблицы размера двойки это, скорее всего, намного лучше, чем 31. Так же, вероятно, для таблицы размера, равного m. - person maaartinus; 05.06.2017
comment
@maaartinus Да, это очевидное свойство, которому должен удовлетворять множитель. Я пытался указать на то, что вы можете легко улучшить его, если посмотрите немного дальше и уменьшите коллизии хэш-кода, немного подумав об этом. И это снижает производительность независимо от размера стола. - person Hans-Peter Störr; 06.06.2017