Оптимизация производительности Java HashMap / альтернатива - PullRequest
98 голосов
/ 18 ноября 2009

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

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

map.get(key)

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

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

Мой метод хеш-кода:

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} равны.

РЕДАКТИРОВАТЬ, некоторые примечания:

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

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

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

Ответы [ 25 ]

4 голосов
/ 19 ноября 2009

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

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

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

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

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

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

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


2 голосов
/ 18 ноября 2009

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

2 голосов
/ 25 июня 2017

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

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

  • Я создал Красное Черное Дерево для 100 000 узлов от 1 до 100 000. Это заняло 785,68 секунды (13 минут). И мне не удалось создать RBTree для 1 миллиона узлов (как ваши результаты с HashMap).
  • Используя "Prime Tree", мой алгоритм структуры данных. Я мог бы построить дерево / карту для 10 миллионов узлов за 21,29 секунды (ОЗУ: 1,97 ГБ). Стоимость ключа поиска составляет O (1).

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


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

1 голос
/ 18 ноября 2009

Другой автор уже указал, что ваша реализация хэш-кода приведет к множеству коллизий из-за способа добавления значений вместе. Я согласен, что если вы посмотрите на объект 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;
1 голос
/ 18 ноября 2009

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

Он хранит все как пара ключей -> значений, это не СУБД. и он стремится быть быстрым.

1 голос
/ 18 ноября 2009

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

1 голос
/ 18 ноября 2009

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

Примерно так:

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;
}

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

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

1 голос
/ 18 ноября 2009

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

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

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

Другим вариантом может быть какой-то механизм базы данных, который легче по сравнению с полноценной СУБД SQL. Что-то вроде Berkeley DB , может быть.

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

1 голос
/ 27 мая 2012

In Effective Java: Руководство по языку программирования (серия Java)

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

Специально:

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

1 голос
/ 21 ноября 2009

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

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

По вашему описанию есть только (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;  

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

...