Оптимизация производительности Java HashMap

Я хочу создать большую HashMap, но производительность put() недостаточно хороша. Любые идеи?

Другие предложения структуры данных приветствуются, но мне нужна функция поиска Java-карты:

map.get(key)

В моем случае я хочу создать карту с 26 миллионами записей. Используя стандартную Java HashMap, ставка ставке становится невыносимо медленной после 2-3 миллионов вставок.

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

Мой метод hashcode:

byte[] a = new byte[2];
byte[] b = new byte[3];
...

public int hashCode() {
    int hash = 503;
    hash = hash * 5381 + (a[0] + a[1]);
    hash = hash * 5381 + (b[0] + b[1] + b[2]);
    return hash;
}

Я использую ассоциативное свойство добавления, чтобы гарантировать, что равные объекты имеют один и тот же хэш-код. Массивы представляют собой байты со значениями в диапазоне от 0 до 51. Значения используются только один раз в любом массиве. Объекты равны, если массивы a содержат одинаковые значения (в любом порядке), и то же самое относится к массиву b. Таким образом, a = {0,1} b = {45,12,33} и a = {1,0} b = {33,45,12} равны.

EDIT, некоторые примечания:

  • Несколько человек критиковали использование хэш-карты или другой структуры данных для хранения 26 миллионов записей. Я не понимаю, почему это кажется странным. Для меня это выглядит как классическая структура данных и алгоритмы. У меня есть 26 миллионов элементов, и я хочу иметь возможность быстро вставлять их и искать их из структуры данных: дать мне структуру данных и алгоритмы.

  • Установка начальной емкости Java HashMap по умолчанию на 26 миллионов снижает производительность.

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

Ответ 1

Как отмечали многие люди, метод hashCode() был виноват. Он генерировал около 20 000 кодов для 26 миллионов различных объектов. Это в среднем 1300 объектов на хэш-ведро = очень очень плохо. Однако, если я превращу два массива в число в базе 52, я гарантированно получаю уникальный хеш-код для каждого объекта:

public int hashCode() {       
    // assume that both a and b are sorted       
    return a[0] + powerOf52(a[1], 1) + powerOf52(b[0], 2) + powerOf52(b[1], 3) + powerOf52(b[2], 4);
}

public static int powerOf52(byte b, int power) {
    int result = b;
    for (int i = 0; i < power; i++) {
        result *= 52;
    }
    return result;
}

Массивы сортируются, чтобы гарантировать, что эти методы удовлетворяют контракту hashCode(), что одинаковые объекты имеют один и тот же хэш-код. Используя старый метод, среднее число статов в секунду по блокам из 100 000 puts, от 100 000 до 2 000 000:

168350.17
109409.195
81344.91
64319.023
53780.79
45931.258
39680.29
34972.676
31354.514
28343.062
25562.371
23850.695
22299.22
20998.006
19797.799
18702.951
17702.434
16832.182
16084.52
15353.083

Использование нового метода дает:

337837.84
337268.12
337078.66
336983.97
313873.2
317460.3
317748.5
320000.0
309704.06
310752.03
312944.5
265780.75
275540.5
264350.44
273522.97
270910.94
279008.7
276285.5
283455.16
289603.25

Намного лучше. Старый метод сработал очень быстро, в то время как новый сохраняет хорошую пропускную способность.

Ответ 2

Одна вещь, которую я замечаю в вашем методе hashCode(), состоит в том, что порядок элементов в массивах a[] и b[] не имеет значения. Таким образом, (a[]={1,2,3}, b[]={99,100}) будет хеш с тем же значением, что и (a[]={3,1,2}, b[]={100,99}). На самом деле все клавиши k1 и k2, где sum(k1.a)==sum(k2.a) и sum(k1.b)=sum(k2.b) приведут к коллизиям. Я предлагаю назначить вес каждой позиции массива:

hash = hash * 5381 + (c0*a[0] + c1*a[1]);
hash = hash * 5381 + (c0*b[0] + c1*b[1] + c3*b[2]);

где c0, c1 и c3 - различные константы (при необходимости вы можете использовать разные константы для b). Это должно выровнять вещи немного больше.

Ответ 3

Разрабатывать Паскаль: вы понимаете, как работает HashMap? У вас есть несколько слотов в вашей хеш-таблице. Хеш-значение для каждой клавиши найдено, а затем отображается на запись в таблице. Если два значения хэша отображаются в одну и ту же запись - "хеш-коллизия" - HashMap строит связанный список.

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

Итак, если вы видите проблемы с производительностью, первое, что я хочу проверить: я получаю случайный вид хэш-кодов? Если нет, вам нужна лучшая хеш-функция. Ну, "лучше" в этом случае может означать "лучше для моего конкретного набора данных". Например, предположим, что вы работаете со строками, и вы взяли длину строки для хеш-значения. (Не так, как работает Java String.hashCode, но я просто составляю простой пример.) Если ваши строки имеют разную длину, от 1 до 10000 и довольно равномерно распределены по этому диапазону, это может быть очень хорошим хэш-функция. Но если ваши строки являются 1 или 2 символами, это будет очень плохой хэш-функцией.

Изменить: я должен добавить: каждый раз, когда вы добавляете новую запись, HashMap проверяет, является ли это дубликат. Когда возникает хеш-столкновение, он должен сравнивать входящий ключ с каждой клавишей, отображаемой на этот слот. Таким образом, в худшем случае, когда все хэши в одном слоте, второй ключ сравнивается с первым ключом, третий ключ сравнивается С# 1 и # 2, четвертый ключ сравнивается С# 1, # 2 и # 3 и т.д. К тому времени, когда вы доберетесь до ключа №1 млн, вы сделали более триллиона сравнений.

@Oscar: Умм, я не вижу, как это "не реально". Это больше похоже на "позвольте мне уточнить". Но да, верно, что если вы создадите новую запись с тем же ключом, что и существующая запись, это перезаписывает первую запись. Это то, что я имел в виду, когда я говорил о поиске дубликатов в последнем абзаце: всякий раз, когда хэши ключей относятся к одному слоту, HashMap должен проверить, является ли это дубликат существующего ключа, или если они находятся только в одном слоте по совпадению хэш-функция. Я не знаю, что это "целая точка" HashMap: я бы сказал, что "целая точка" заключается в том, что вы можете быстро извлекать элементы по клавишам.

Но так или иначе, это не влияет на "целую точку", которую я пытался сделать: когда у вас есть два ключа - да, разные клавиши, а не тот же самый ключ, который появляется снова - это карта в тот же слот в таблице HashMap создает связанный список. Затем, поскольку он должен проверять каждый новый ключ, чтобы убедиться, что он фактически является дубликатом существующего ключа, каждая попытка добавить новую запись, которая сопоставляется с этим же слотом, должна преследовать связанный список, проверяющий каждую существующую запись, чтобы увидеть, является дубликатом ранее увиденного ключа, или если это новый ключ.

Обновление долго после исходного сообщения

Я только что получил голосование по этому вопросу через 6 лет после публикации, что заставило меня перечитать вопрос.

Хеш-функция, заданная в вопросе, не является хорошим хешем для 26 миллионов записей.

Он добавляет вместе [0] + a [1] и b [0] + b [1] + b [2]. Он говорит, что значения каждого байта варьируются от 0 до 51, так что дает только (51 * 2 + 1) * (51 * 3 + 1) = 15 862 возможных хэш-значений. С 26 миллионами записей это означает в среднем около 1639 записей на хэш-значение. Это много-много коллизий, требующих много-много последовательных поисков через связанные списки.

OP говорит, что разные порядки в массиве a и array b должны считаться равными, т.е. [[1,2], [3,4,5]]. equals ([[2,1], [5,3, 4]]), и поэтому для выполнения контракта они должны иметь равные хэш-коды. Хорошо. Тем не менее, существует более 15 000 возможных значений. Его вторая предложенная хэш-функция намного лучше, давая более широкий диапазон.

Хотя, как комментировал кто-то другой, кажется, что хэш-функция не подходит для изменения других данных. Было бы разумнее "нормализовать" объект при его создании или использовать хэш-функцию из копий массивов. Кроме того, использование цикла для вычисления констант каждый раз через функцию неэффективно. Поскольку здесь всего четыре значения, я бы либо написал

return a[0]+a[1]*52+b[0]*52*52+b[1]*52*52*52+b[2]*52*52*52*52;

который заставит компилятор выполнить вычисление один раз во время компиляции; или имеют 4 статические константы, определенные в классе.

Кроме того, первый черновик в хэш-функции имеет несколько расчетов, которые ничего не делают для добавления в диапазон выходов. Обратите внимание, что он сначала устанавливает hash = 503, а умножает на 5381, даже учитывая значения из класса. Итак... по сути он добавляет 503 * 5381 к каждому значению. Что это делает? Добавление константы к каждому значению хэша просто сжигает циклы процессора, не делая ничего полезного. Урок здесь: добавление сложности в хэш-функцию не является целью. Цель состоит в том, чтобы получить широкий диапазон различных значений, а не просто добавить сложность для сложности.

Ответ 4

Моя первая идея - убедиться, что вы правильно инициализируете свой HashMap. Из JavaDocs для HashMap:

У экземпляра HashMap есть два параметра, которые влияют на его производительность: начальная емкость и коэффициент загрузки. Емкость - это количество ведер в хэш-таблице, а начальная емкость - это просто емкость на момент создания хеш-таблицы. Фактор нагрузки - это показатель того, насколько полная хэш-таблица может быть получена до того, как ее мощность будет автоматически увеличена. Когда количество записей в хэш-таблице превышает произведение коэффициента загрузки и текущей емкости, хеш-таблица повторно отображается (то есть внутренние структуры данных перестраиваются), так что хэш-таблица имеет примерно вдвое больше количества ковшей.

Итак, если вы начинаете с слишком маленького HashMap, то каждый раз, когда ему нужно изменить размер, все хеши пересчитываются... что может быть тем, что вы чувствуете, когда вы перейдите к 2-3 миллионам точек ввода.

Ответ 5

Я бы предложил трехсторонний подход:

  • Запустите Java с большим объемом памяти: java -Xmx256M например, для работы с 256 мегабайтами. Используйте больше, если необходимо, и у вас много ОЗУ.

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

  • Используйте лучший алгоритм хэширования. Тот, который вы опубликовали, вернет тот же хеш, где a = {0, 1}, как и где a = {1, 0}, причем все остальные равны.

Используйте то, что Java дает вам бесплатно.

public int hashCode() {
    return 31 * Arrays.hashCode(a) + Arrays.hashCode(b);
}

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

Ответ 6

Переход в серое пространство "темы включения/выключения", но необходимо устранить путаницу в отношении предложения Оскара Рейеса о том, что большее количество столкновений с хэшем - это хорошо, потому что это уменьшает количество элементов в HashMap. Я могу неправильно понять, что говорит Оскар, но я, похоже, не единственный: kdgregory, delfuego, Nash0, и я, кажется, разделяю одно и то же (неверное) понимание.

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

Пример Java pastebin в http://pastebin.com/f20af40b9, кажется, указывает на то, что выше правильно суммирует то, что предлагает Оскар.

Независимо от понимания или непонимания, то, что происходит, это разные экземпляры одного и того же класса, которые не вставлены только один раз в HashMap, если они имеют один и тот же хэш-код - пока он не определит, являются ли ключи равными или нет. Контракт hashcode требует, чтобы равные объекты имели один и тот же хэш-код; однако он не требует, чтобы неравные объекты имели разные хэш-коды (хотя это может быть желательно по другим причинам) [1].

Пример pastebin.com/f20af40b9 (который, по крайней мере, дважды упоминается Оскаром) следует, но слегка модифицирован для использования утверждений JUnit, а не для строк печати. Этот пример используется для поддержки предложения о том, что одни и те же хэш-коды вызывают конфликты, и когда классы одинаковы, создается только одна запись (например, только одна строка в этом конкретном случае):

@Test
public void shouldOverwriteWhenEqualAndHashcodeSame() {
    String s = new String("ese");
    String ese = new String("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // AND equal
    assertTrue(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(2, map.size());

    assertEquals(2, map.get("ese"));
    assertEquals(3, map.get(some));

    assertTrue(s.equals(ese) && s.equals("ese"));
}

class SomeClass {
    public int hashCode() {
        return 100727;
    }
}

Однако хэш-код не является полной историей. То, что игнорирует пример pastebin, состоит в том, что оба s и ese равны: они являются строкой "ese". Таким образом, вставка или получение содержимого карты с использованием s или ese или "ese" в качестве ключа эквивалентны, поскольку s.equals(ese) && s.equals("ese").

Второй тест показывает, что ошибочно заключить, что идентичные хэш-коды одного класса - это причина, по которой ключ → значение s -> 1 перезаписывается ese -> 2, когда map.put(ese, 2) вызывается в тестовом. В тесте два s и ese все еще имеют один и тот же хэш-код (как проверено assertEquals(s.hashCode(), ese.hashCode());) И они являются одним и тем же классом. Тем не менее, s и ese являются экземплярами MyString в этом тесте, а не Java String экземплярами - с той лишь разницей, что для этого теста является равным: String s equals String ese в тесте выше, тогда как MyStrings s does not equal MyString ese в проверьте два:

@Test
public void shouldInsertWhenNotEqualAndHashcodeSame() {
    MyString s = new MyString("ese");
    MyString ese = new MyString("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // BUT not equal
    assertFalse(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(3, map.size());

    assertEquals(1, map.get(s));
    assertEquals(2, map.get(ese));
    assertEquals(3, map.get(some));
}

/**
 * NOTE: equals is not overridden so the default implementation is used
 * which means objects are only equal if they're the same instance, whereas
 * the actual Java String class compares the value of its contents.
 */
class MyString {
    String i;

    MyString(String i) {
        this.i = i;
    }

    @Override
    public int hashCode() {
        return 100727;
    }
}

Основываясь на более позднем комментарии, Оскар, похоже, отменил сказанное ранее и признал важность равных. Тем не менее, по-прежнему кажется, что вопрос о том, что равен, что имеет значение, а не "тот же класс", неясен (акцент мой):

"Не совсем. Список создается только в том случае, если хэш один и тот же, но ключ отличается. Например, если String дает hashcode 2345, а Integer дает тот же хэш-код 2345, то целое число вставляется в список потому что String.equals(Integer) является ложным, но , если у вас есть тот же класс (или, по крайней мере, равно., возвращает true), тогда используется одна и та же запись. Например, новые String (" one ") и `new String (" one "), используемый в качестве ключей, будет использовать одну и ту же запись. Фактически это ТОЧНАЯ точка HashMap на первом месте! Посмотрите сами: pastebin.com/f20af40b9 - Oscar Reyes"

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

"@delfuego: Посмотрите сами: pastebin.com/f20af40b9 Итак, в этом вопросе используется один и тот же класс (подождите минуту, тот же класс используется правильно?) Это подразумевает, что при использовании одного и того же хэша используется одна и та же запись, и нет" списка "записей. - Oscar Reyes"

или

"На самом деле это увеличило бы производительность. Чем больше коллизий, тем меньше записей в элементе hashtable меньше, чем нужно. Не хэш (который выглядит отлично), ни хеш-таблица (которая отлично работает) Я бы поспорил, что это на создание объекта, где производительность ухудшается. - Oscar Reyes"

или

"@kdgregory: Да, но только если столкновение происходит с разными классами, для одного и того же класса (в этом случае) используется одна и та же запись. - Oscar Reyes"

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


[1] - Из эффективной Java, второе издание Джошуа Блоха:

  • Всякий раз, когда он вызывается одним и тем же объектом более одного раза во время выполнения приложения, метод hashCode должен последовательно возвращать одинаковое целое число, при условии, что никакая информация, используемая при равных сравнениях на объект изменен. Это целое число не должно оставаться согласованным с одним исполнением приложения на другое выполнение того же приложения.

  • Если два объекта равны в соответствии с методом s (Obj ect), то вызов метода hashCode для каждого из двух объектов должен целочисленный результат.

  • Не требуется, чтобы два объекта были неравными в соответствии с методом s (Object), а затем вызывали метод hashCode на каждом из двух объектов должен производить четкие целочисленные результаты. Однако программист должен осознавая, что получение отдельных целочисленных результатов для неравных объектов может улучшить производительность хэш-таблиц.

Ответ 7

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

a [0] + a [1] всегда будет между 0 и 512. добавление b всегда приведет к числу от 0 до 768. умножьте их, и вы получите верхний предел 400 000 уникальных комбинаций, предполагая, что ваши данные отлично распределены между всеми возможными значениями каждого байта. Если ваши данные являются обычными, у вас, вероятно, гораздо меньше уникальных результатов этого метода.

Ответ 8

HashMap имеет начальную емкость, а производительность HashMap очень сильно зависит от hashCode, который создает базовые объекты.

Попробуйте настроить оба.

Ответ 9

Если у них есть какой-либо шаблон, вы можете разбить карту на более мелкие карты и иметь карту индексов.

Пример: Ключи: 1,2,3,.... n 28 карт по 1 миллион каждый. Карта указателей: 1-1 000 000 → Map1 1,000,000-2,000,000 → Map2

Итак, вы будете делать два поиска, но набор ключей будет 1,000,000 против 28 000 000. Вы также можете легко сделать это с помощью шаблонов укусов.

Если ключи полностью случайны, это не будет работать

Ответ 10

Если два байтовых массива, которые вы упоминаете, - это ваш весь ключ, значения находятся в диапазоне 0-51, уникальные, а порядок в массивах a и b незначителен, моя математика говорит мне, что всего лишь около 26 миллионов возможные перестановки и что вы, вероятно, пытаетесь заполнить карту значениями для всех возможных ключей.

В этом случае как заполнение, так и получение значений из вашего хранилища данных, конечно, будут намного быстрее, если вы используете массив вместо HashMap и индексируете его от 0 до 25989599.

Ответ 11

Я опаздываю здесь, но пару комментариев о больших картах:

  • Как обсуждалось подробно в других сообщениях, с хорошим hashCode(), 26M записей на карте не имеет большого значения.
  • Однако потенциально скрытая проблема здесь - это воздействие GC гигантских карт.

Я делаю предположение, что эти карты долговечны. т.е. вы заполняете их, и они остаются на протяжении всего приложения. Я также предполагаю, что само приложение долгое время - как сервер какого-то типа.

Каждая запись в Java HashMap требует трех объектов: ключ, значение и запись, которая связывает их вместе. Таким образом, 26M записей на карте означает объекты 26M * 3 == 78M. Это нормально, пока вы не нажмете полный GC. Тогда у вас проблема с паузой. GC будет смотреть на каждый из 78M-объектов и определять, что они все живы. Объекты 78M + - это просто много объектов, на которые нужно смотреть. Если ваше приложение может терпеть случайные длительные (возможно, много секунд) паузы, нет никаких проблем. Если вы пытаетесь добиться каких-либо гарантий задержек, у вас может возникнуть серьезная проблема (конечно, если вы хотите гарантировать латентность, Java не на платформе, чтобы выбрать:)) Если значения в ваших картах быстро оттолкнут, вы можете получить частые полные собрания что значительно осложняет проблему.

Я не знаю отличного решения этой проблемы. Идеи:

  • Иногда возможно настраивать размеры GC и кучи, чтобы "в основном" предотвратить полные GC.
  • Если ваше содержимое карты сильно изломилось, вы можете попробовать Javolution FastMap - он может объединять объекты Entry, что может снизить частоту полного улавливается
  • Вы можете создать свой собственный имплант карты и сделать явное управление памятью на байт [] (т.е. торговое CPU для более предсказуемой задержки, сериализуя миллионы объектов в один байт [] - ugh!)
  • Не используйте Java для этой части - поговорите с какой-то предсказуемой БД в памяти через сокет
  • Надеемся, что новый сборщик G1 поможет (в основном, относится к случаю с высокой ошибкой)

Просто некоторые мысли от человека, который провел много времени с гигантскими картами на Java.


Ответ 12

Вы можете попытаться использовать базу данных в памяти, например HSQLDB.

Ответ 13

В моем случае я хочу создать карту с 26 миллионами записей. Используя стандартную Java HashMap, ставка ставке становится невыносимо медленной после 2-3 миллионов вставок.

Из моего эксперимента (студенческий проект в 2009 году):

  • Я создал Red Black Tree для 100.000 узлов от 1 до 100.000. Это заняло 785,68 секунды (13 минут). И мне не удалось создать RBTree для 1 миллиона узлов (например, ваши результаты с помощью HashMap).
  • Использование "Prime Tree", моей структуры данных алгоритма. Я мог бы построить дерево/карту для 10 миллионов узлов в течение 21.29 секунд (RAM: 1.97Gb). Ключевая стоимость поиска - O (1).

Примечание: "Prime Tree" лучше всего работает на "непрерывных клавишах" от 1 до 10 миллионов. Для работы с такими ключами, как HashMap, нам нужна настройка несовершеннолетних.


Итак, что такое #PrimeTree? Короче говоря, это древовидная структура данных, такая как двоичное дерево, с номерами ветвей - это простые числа (вместо "2" - двоичные).

Ответ 14

Рассматривали ли вы использование встроенной базы данных для этого. Посмотрите Berkeley DB. Это открытый источник, принадлежащий Oracle сейчас.

Он хранит все как пару Key- > Value, это не СУБД. и он стремится быть быстрым.

Ответ 15

SQLite позволяет использовать его в памяти.

Ответ 16

Сначала вы должны проверить, что вы правильно используете Map, хороший метод hashCode() для ключей, начальную емкость для Map, правильную реализацию карты и т.д., как описано во многих других ответах.

Тогда я бы предложил использовать профилировщик, чтобы увидеть, что на самом деле происходит, и где тратится время выполнения. Например, метод hashCode() выполняется в миллиарды раз?

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

Другим вариантом будет некоторый механизм базы данных, который будет легче, чем полная SQL-RDBMS. Что-то вроде Berkeley DB, возможно.

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

Ответ 17

Вы можете попытаться кэшировать вычисленный хэш-код ключевому объекту.

Что-то вроде этого:

public int hashCode() {
  if(this.hashCode == null) {
     this.hashCode = computeHashCode();
  }
  return this.hashCode;
}

private int computeHashCode() {
   int hash = 503;
   hash = hash * 5381 + (a[0] + a[1]);
   hash = hash * 5381 + (b[0] + b[1] + b[2]);
   return hash;
}

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

Изменить: Кажется, что кеширование имеет значения кода не стоит, когда вы добавляете каждый ключ только один раз на карту. В какой-то другой ситуации это может быть полезно.

Ответ 18

Еще один плакат уже указал, что реализация вашего хэш-кода приведет к большому количеству столкновений из-за того, что вы добавляете значения вместе. Я готов быть таким, если вы посмотрите на объект HashMap в отладчике, вы обнаружите, что у вас может быть 200 различных значений хэша с чрезвычайно длинными цепочками ведра.

Если у вас всегда есть значения в диапазоне 0..51, каждое из этих значений будет принимать 6 бит для представления. Если у вас всегда есть 5 значений, вы можете создать 30-битный хэш-код с левыми сменами и добавлениями:

    int code = a[0];
    code = (code << 6) + a[1];
    code = (code << 6) + b[0];
    code = (code << 6) + b[1];
    code = (code << 6) + b[2];
    return code;

Левый сдвиг выполняется быстро, но оставит вас с хэш-кодами, которые распределены неравномерно (потому что 6 бит означает диапазон 0..63). Альтернативой является умножение хэша на 51 и добавление каждого значения. Это все еще не будет идеально распределено (например, {2,0} и {1,52} будут сталкиваться) и будут медленнее сдвига.

    int code = a[0];
    code *= 51 + a[1];
    code *= 51 + b[0];
    code *= 51 + b[1];
    code *= 51 + b[2];
    return code;

Ответ 19

Как указано, ваша реализация hashcode имеет слишком много коллизий, и ее исправление должно привести к достойной производительности. Более того, кэширование хэш-кодов и эффективное использование эквивалентов будет способствовать.

Если вам нужно еще больше оптимизировать:

По вашему описанию есть только (52 * 51/2) * (52 * 51 * 50/6) = 29304600 различных ключей (из которых 26000000, т.е. около 90%, будут присутствовать). Таким образом, вы можете создать хеш-функцию без каких-либо коллизий и использовать простой массив, а не хэш-карту, чтобы хранить ваши данные, уменьшая потребление памяти и увеличивая скорость поиска:

T[] array = new T[Key.maxHashCode];

void put(Key k, T value) {
    array[k.hashCode()] = value;

T get(Key k) {
    return array[k.hashCode()];
}

(Как правило, невозможно создать эффективную хеш-функцию без столкновений, которая хорошо кластеризуется, поэтому HashMap будет терпеть столкновения, которые накладывают некоторые накладные расходы)

Предполагая, что a и b отсортированы, вы можете использовать следующую хеш-функцию:

public int hashCode() {
    assert a[0] < a[1]; 
    int ahash = a[1] * a[1] / 2 
              + a[0];

    assert b[0] < b[1] && b[1] < b[2];

    int bhash = b[2] * b[2] * b[2] / 6
              + b[1] * b[1] / 2
              + b[0];
    return bhash * 52 * 52 / 2 + ahash;
}

static final int maxHashCode = 52 * 52 / 2 * 52 * 52 * 52 / 6;  

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

Ответ 20

В Эффективное Java: Руководство по языку программирования (Серия Java)

В главе 3 вы можете найти хорошие правила для вычисления hashCode().

Специально:

Если поле является массивом, рассматривайте его так, как будто каждый элемент является отдельным полем. То есть вычислить хэш-код для каждого значимого элемента, применяя эти правила рекурсивно и объединить эти значения на шаг 2.b. Если каждый элемент в поле массива является значительным, вы можете использовать один из Методы Arrays.hashCode добавлены в версию 1.5.

Ответ 21

Выделите большую карту в начале. Если вы знаете, что у него будет 26 миллионов записей, и у вас есть память для него, сделайте new HashMap(30000000).

Вы уверены, что у вас достаточно памяти для 26 миллионов записей с 26 миллионами ключей и значений? Для меня это звучит много. Вы уверены, что сбор мусора все еще стоит на вашей отметке от 2 до 3 миллионов? Я мог представить это как узкое место.

Ответ 22

Вы можете попробовать две вещи:

  • Сделайте метод hashCode более простым и эффективным, например, последовательным int

  • Инициализируйте свою карту как:

    Map map = new HashMap( 30000000, .95f );
    

Эти два действия значительно уменьшат количество перефразирования структуры, и довольно легко проверить, я думаю.

Если это не сработает, рассмотрите возможность использования другого хранилища, такого как РСУБД.

ИЗМЕНИТЬ

Странно, что установка начальной емкости снижает производительность в вашем случае.

Смотрите javadocs:

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

Я сделал microbeachmark (который никоим образом не является окончательным, но, по крайней мере, доказывает эту точку)

$cat Huge*java
import java.util.*;
public class Huge {
    public static void main( String [] args ) {
        Map map = new HashMap( 30000000 , 0.95f );
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
import java.util.*;
public class Huge2 {
    public static void main( String [] args ) {
        Map map = new HashMap();
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
$time java -Xms2g -Xmx2g Huge

real    0m16.207s
user    0m14.761s
sys 0m1.377s
$time java -Xms2g -Xmx2g Huge2

real    0m21.781s
user    0m20.045s
sys 0m1.656s
$

Таким образом, использование начальной емкости падает с 21 до 16 секунд из-за повторной обработки. Это оставляет нас с вашим методом hashCode как "область возможностей";)

Забастовкa > ИЗМЕНИТЬ

Это не HashMap

Согласно вашему последнему изданию.

Я думаю, вы должны действительно профилировать свое приложение и видеть, где он потребляет память/процессор.

Я создал класс, реализующий ваш те же hashCode

Этот хэш-код дает миллионы коллизий, тогда записи в HashMap резко сокращаются.

Я перехожу от 21s, 16s в моем предыдущем тесте к 10s и 8s. Причина в том, что hashCode вызывает большое количество столкновений, и вы не сохраняете объекты 26M, которые вы думаете, но значительно меньшее число (примерно 20 тыс. Я бы сказал) Итак:

Проблемы НЕ ХАШМАТ находятся где-то еще в вашем коде.

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

Здесь моя реализация вашего класса.

note Я не использовал диапазон 0-51, как вы, но от -126 до 127 для моих значений и допускает повторение, потому что я сделал этот тест, прежде чем вы обновили свой вопрос

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

import java.util.*;
public class Item {

    private static byte w = Byte.MIN_VALUE;
    private static byte x = Byte.MIN_VALUE;
    private static byte y = Byte.MIN_VALUE;
    private static byte z = Byte.MIN_VALUE;

    // Just to avoid typing :) 
    private static final byte M = Byte.MAX_VALUE;
    private static final byte m = Byte.MIN_VALUE;


    private byte [] a = new byte[2];
    private byte [] b = new byte[3];

    public Item () {
        // make a different value for the bytes
        increment();
        a[0] = z;        a[1] = y;    
        b[0] = x;        b[1] = w;   b[2] = z;
    }

    private static void increment() {
        z++;
        if( z == M ) {
            z = m;
            y++;
        }
        if( y == M ) {
            y = m;
            x++;
        }
        if( x == M ) {
            x = m;
            w++;
        }
    }
    public String toString() {
        return "" + this.hashCode();
    }



    public int hashCode() {
        int hash = 503;
        hash = hash * 5381 + (a[0] + a[1]);
        hash = hash * 5381 + (b[0] + b[1] + b[2]);
        return hash;
    }
    // I don't realy care about this right now. 
    public boolean equals( Object other ) {
        return this.hashCode() == other.hashCode();
    }

    // print how many collisions do we have in 26M items.
    public static void main( String [] args ) {
        Set set = new HashSet();
        int collisions = 0;
        for ( int i = 0 ; i < 26000000 ; i++ ) {
            if( ! set.add( new Item() ) ) {
                collisions++;
            }
        }
        System.out.println( collisions );
    }
}

Использование этого класса имеет ключ для предыдущей программы

 map.put( new Item() , i );

дает мне:

real     0m11.188s
user     0m10.784s
sys 0m0.261s


real     0m9.348s
user     0m9.071s
sys  0m0.161s

Ответ 24

Я сделал небольшой тест некоторое время назад со списком против хэш-карты, забавная вещь повторялась через список, и поиск объекта занимал такое же количество времени в миллисекундах, что и использование функции hashmaps get... просто fyi. О, да, память - большая проблема при работе с хэшмапами такого размера.

Ответ 25

Используемые популярные методы хеширования на самом деле не очень хороши для больших наборов, и, как указано выше, используемый хэш особенно плох. Лучше использовать алгоритм хэширования с высоким уровнем микширования и охвата, такой как BuzHash (реализация примера на http://www.java2s.com/Code/Java/Development-Class/AveryefficientjavahashalgorithmbasedontheBuzHashalgoritm.htm)