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


102

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

Інші пропозиції щодо структури даних вітаються, але мені потрібна функція пошуку Java-карти:

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. Значення використовуються лише один раз в будь-якому масиві. Об'єкти рівні, якщо масиви містять однакові значення (в будь-якому порядку) і те ж саме стосується масиву b. Тож a = {0,1} b = {45,12,33} і a = {1,0} b = {33,45,12} рівні.

EDIT, деякі примітки:

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

  • Встановлення початкової ємності Java HashMap за замовчуванням до 26 мільйонів знижує продуктивність.

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


29
Якщо HashMap стає повільним, швидше за все, ваша хеш-функція недостатньо хороша.
Паскаль Куок

12
лікарю, боляче, коли я це
skaffman

12
Це справді гарне запитання; приємна демонстрація того, чому алгоритми хешування мають значення і який вплив вони можуть мати на продуктивність
oxbow_lakes

12
Сума значень a має діапазон від 0 до 102, а сума b має діапазон від 0 до 153, тому у вас є лише 15 606 можливих хеш-значень і в середньому 1666 ключів з тим же хеш-кодом. Ви повинні змінити свій хеш-код, щоб кількість можливих хеш-кодів була значно більшою, ніж кількість ключів.
Пітер Лоурі

6
Я психічно визначив, що ви моделюєте Texas Hold 'Em Poker ;-)
bacar

Відповіді:


56

Як багато людей зазначали, у цьому 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 путів, 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

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


17
Я пропоную не змінювати масиви в hashCodeметоді. За умовою, hashCodeстан об'єкта не змінює. Можливо, конструктор буде кращим місцем для їх сортування.
Майкл Майерс

Я згоден, що сортування масивів має відбуватися в конструкторі. Показаний код ніколи не встановлює хеш-код. Розрахунок коду можна зробити простіше наступним чином : int result = a[0]; result = result * 52 + a[1]; //etc.
rsp

Я згоден, що сортування в конструкторі, а потім обчислення хеш-коду, як підказують mmyers та rsp, є кращим. У моєму випадку моє рішення прийнятне, і я хотів підкреслити той факт, що масиви повинні бути відсортовані для hashCode()роботи.
наш

3
Зауважте, що ви також можете кешувати хеш-код (і визнати його недійсним відповідним чином, якщо ваш об'єкт є змінним).
NateS

1
Просто використовуйте java.util.Arrays.hashCode () . Це простіше (без коду писати і підтримувати самостійно), його обчислення, ймовірно, швидше (менше множення), а розподіл його хеш-кодів, ймовірно, буде більш рівномірним.
jcsahnwaldt Відновити Моніку

18

Одна річ , яку я помічаю в вашому 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якщо це необхідно). Це навіть повинно викласти речі трохи більше.


Хоча я також повинен додати, що це не буде працювати для мене, тому що я хочу, щоб властивості, що масиви з однаковими елементами в різних порядках давали один і той же хеш-код.
наш

5
У цьому випадку у вас є хеш-коди 52C2 + 52C3 (за моїм калькулятором 23426), і хешмап - це дуже неправильний інструмент для роботи.
kdgregory

Насправді це збільшило б продуктивність. Чим більше зіткнень екв., Тим менше записів у хешбелевому еквіваленті. менше роботи. Це не хеш (який виглядає чудово), ні хешбел (який чудово працює) Я б ставку, що це на створенні об'єкта, де продуктивність деградує.
OscarRyz

7
@Oscar - більше зіткнень дорівнює більшій роботі, адже тепер вам доведеться виконати лінійний пошук хеш-ланцюга. Якщо у вас є 26 000 000 різних значень на рівні (), і 26 000 різних значень на хеш-код (), то ланцюги ковшів матимуть 1000 об'єктів кожен.
kdgregory

@ Nash0: Ви, здається, говорите, що хочете, щоб вони мали однаковий хеш-код, але в той же час були не рівними (як визначено методом equals ()). Чому б ти цього хотів?
МАК

17

Детальніше про Pascal: Ви розумієте, як працює HashMap? У вашій хеш-таблиці є деяка кількість слотів. Значення хеш-класу для кожного ключа знайдено, а потім відображено до запису в таблиці. Якщо два значення хеш-пам'яті відображаються в одному записі - "хеш-зіткнення" - HashMap створює пов'язаний список.

Хеш-зіткнення можуть вбити продуктивність хеш-карти. У крайньому випадку, якщо всі ваші ключі мають однаковий хеш-код, або якщо вони мають різні хеш-коди, але всі вони відображаються в одному слоті, то ваша хеш-карта перетворюється на пов'язаний список.

Тож якщо у вас виникають проблеми з продуктивністю, перше, що я перевірю, це: чи отримую я випадковий розподіл хеш-кодів? Якщо ні, то вам потрібна краща хеш-функція. Ну, "краще" в цьому випадку може означати "краще для мого конкретного набору даних". Припустимо, ви працювали з рядками, і ви взяли довжину рядка для хеш-значення. (Не так, як працює String.hashCode Java, але я лише складаю простий приклад.) Якщо ваші рядки мають різну довжину, від 1 до 10 000, і досить рівномірно розподілені по цьому діапазону, це може бути дуже добре хеш-функція. Але якщо у ваших рядках всі 1 або 2 символи, це буде дуже поганою хеш-функцією.

Редагувати: Я повинен додати: Щоразу, коли ви додаєте новий запис, HashMap перевіряє, чи це дублікат. Коли відбувається хеш-зіткнення, він повинен порівнювати вхідний ключ з кожним ключем, зіставленим із цим слотом. Так що в гіршому випадку, коли все хеширується в одному слоті, другий ключ порівнюється з першим ключем, третій ключ порівнюється з №1 і №2, четвертий ключ порівнюється з №1, №2 і №3 і т. д. До моменту, коли ви перейдете до ключового # мільйона, ви зробили понад трильйон порівнянь.

@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 записів на хеш-значення. Це багато і багато зіткнень, що вимагають багато-багато послідовних пошуків через пов'язані списки.

ОП говорить, що різні порядки в масиві a та масиві b слід вважати рівними, тобто [[1,2], [3,4,5]]. Дорівнює ([[2,1], [5,3,4] ]) і тому для виконання договору вони повинні мати рівні хеш-коди. Добре. Тим не менш, існує набагато більше 15000 можливих значень. Його друга запропонована хеш-функція значно краща, даючи ширший діапазон.

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

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

що змусить компілятор виконати обчислення один раз у час компіляції; або мають 4 статичні константи, визначені в класі.

Крім того, перша чернетка на хеш-функції має кілька обчислень, які нічого не роблять для додавання до діапазону виходів. Зверніть увагу, що він спочатку встановлює хеш = 503, ніж помножує на 5381, перш ніж навіть розглядати значення з класу. Отже ... фактично він додає 503 * 5381 до кожної цінності. Що це досягає? Додавання константи до кожного значення хешу просто спалює цикли процесора, не виконуючи нічого корисного. Урок тут: Додавання складності хеш-функції - не мета. Мета - отримати широкий спектр різних значень, а не просто додати складність заради складності.


3
Так, погана хеш-функція призведе до такої поведінки. +1
Хеннінг

Не зовсім. Список створюється лише в тому випадку, якщо хеш однаковий, але ключ інший . Наприклад, якщо String дає хеш-код 2345, а Integer дає той самий хеш-код 2345, тоді ціле число вставляється в список, оскільки String.equals( Integer )є false. Але якщо у вас той самий клас (або принаймні .equalsповертає істину), тоді використовується той самий запис. Наприклад, new String("one")і `new String (" один "), що використовується в якості ключів, буде використовувати один і той же запис. Насправді це ВСЕ точка HashMap в першу чергу! Побачте самі: pastebin.com/f20af40b9
OscarRyz

3
@Oscar: Дивіться мою відповідь, додану до мого початкового повідомлення.
Джей

Я знаю, що це дуже стара тема, але ось посилання на термін "зіткнення", оскільки це стосується хеш-кодів: посилання . Коли ви замінюєте значення в хешмапі, додаючи інше значення тим самим ключем, це не називається зіткненням
Тахір Ахтар

@Tahir Рівно. Можливо, моя посада була погано сформульована. Дякуємо за роз’яснення.
Джей

7

Моя перша ідея - переконатися, що ви ініціалізуєте свій HashMap належним чином. З JavaDocs для HashMap :

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

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


Я не думаю, що вони перераховуються ніколи. Розмір столу збільшується, хеші зберігаються.
Хеннінг

Hashmap просто робить трохи розумним і для кожного запису: newIndex = зберігається Hash & newLength;
Хеннінг

4
Хеннінг: Можливо, неправильне формулювання з боку дельфуего, але справа справедлива. Так, значення хешу не перераховуються в тому сенсі, що вихід hashCode () не перераховується. Але коли розмір таблиці збільшується, всі ключі повинні бути знову вставлені в таблицю, тобто хеш-значення потрібно повторно хешировать, щоб отримати новий номер слота в таблиці.
Джей

Джей, так - погане формулювання справді, і те, що ви сказали. :)
delfuego

1
@delfuego та @ nash0: Так, встановлення початкової ємності, що дорівнює кількості елементів, знижує продуктивність, тому що у вас є зіткнення мільйонів, і, таким чином, ви використовуєте лише невелику кількість цієї ємності. Навіть якщо ви використовуєте всі наявні записи, встановлення однакової ємності зробить це найгірше !, тому що через коефіцієнт навантаження буде запропоновано більше місця. Вам доведеться використовувати initialcapactity = maxentries/loadcapacity(наприклад, 30 М, 0,95 для записів 26М), але це НЕ ваш випадок, оскільки у вас виникають усі ті зіткнення, які ви використовуєте лише приблизно 20 кб або менше.
OscarRyz

7

Я б запропонував тристоронній підхід:

  1. Запустіть Java з більшою кількістю пам'яті: java -Xmx256Mнаприклад, для роботи з 256 мегабайт. Використовуйте більше, якщо потрібно, і у вас є багато оперативної пам’яті.

  2. Кешуйте свої обчислені хеш-значення, як запропоновано іншим плакатом, тому кожен об'єкт обчислює його хеш-значення лише один раз.

  3. Використовуйте кращий алгоритм хешування. Ви розмістили той самий хеш, де a = {0, 1}, як і де a = {1, 0}, а всі інші рівні.

Використовуйте те, що дає Java безкоштовно.

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

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


Оперативна пам’ять може бути малою для таких карт і масивів, тому я вже підозрював проблему обмеження пам'яті.
ReneS

7

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

Якщо я розумію, що говорить Оскар про той самий клас із тим самим хеш-кодом, він пропонує лише один екземпляр класу із заданим хеш-кодом вставити в HashMap. Наприклад, якщо у мене є екземпляр SomeClass з хеш-кодом 1 і другий екземпляр SomeClass з хеш-кодом 1, вставляється лише один екземпляр SomeClass.

Приклад пастбіну Java на веб-сайті http://pastebin.com/f20af40b9, схоже, вказує на сказане вище, резюмує те, що пропонує Оскар.

Незалежно від розуміння чи непорозуміння, що трапляється в різних примірниках одного класу, не вставляйте лише один раз у HashMap, якщо вони мають один і той же хеш-код - не поки не буде визначено, чи рівні ключі чи ні. Контракт хеш-коду вимагає, щоб рівні об'єкти мали однаковий хеш-код; однак, не потрібно, щоб у неоднакових об’єктів були різні хеш-коди (хоча це може бути бажано з інших причин) [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;
    }
}

Однак хеш-код не є повною історією. Що зневажає приклад пастбіну, це той факт, що обидва sі eseрівні: вони обидва є рядком "ese". Таким чином, вставка або отримання вмісту карти з допомогою sабо eseчи в "ese"якості ключа все еквівалентні , так як s.equals(ese) && s.equals("ese").

Другий тест демонструє помилковість висновку, що однакові хеш-коди одного класу є причиною s -> 1перезапису значень key -> , ese -> 2коли map.put(ese, 2)викликається в тестовому. У тесті два, sі eseдосі є однаковий хеш-код (як це перевірено assertEquals(s.hashCode(), ese.hashCode());) І вони одного класу. Однак, sі eseце MyStringвипадки цього тесту, а не Stringекземпляри Java - єдиною різницею, що стосується цього тесту, є рівно: 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 дає хеш-код 2345, а Integer дає той самий хеш-код 2345, тоді ціле число вставляється в список через String. equals (Integer) - помилково. Але якщо у вас той самий клас (або принаймні .equals повертається true), використовується той самий запис. Наприклад, новий String ("one") і `new String (" one "), який використовується як ключі, буде використовувати той самий запис. Насправді це в цілому точка HashMap! Перш за все, переконайтесь у цьому: pastebin.com/f20af40b9 - Оскар Рейєс "

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

"@delfuego: Побачте самі: pastebin.com/f20af40b9 Отже, у цьому питанні використовується той самий клас (почекайте хвилину, той самий клас використовується правильно?), що означає, що коли для того ж хеша використовується той самий запис використовується і немає "списку" записів. - Оскар Рейєс "

або

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

або

"@kdgregory: Так, але тільки якщо зіткнення трапляється з різними класами, для одного і того ж класу (у тому випадку) використовується один і той же запис. - Оскар Рейєс"

Знову ж таки, я можу неправильно зрозуміти, що насправді намагався сказати Оскар. Однак його оригінальні коментарі викликали достатню плутанину, що здається доцільним все прояснити явними тестами, щоб не було затяжних сумнівів.


[1] - З ефективної Java, друге видання Джошуа Блоха:

  • Кожного разу, коли він під час виконання програми викликає один і той самий об'єкт не один раз, метод hashCode повинен послідовно повертати одне ціле ціле число, за умови, що жодна інформація, що використовується в однакових порівняннях об'єкта, не змінюється. Це ціле число не повинно залишатися послідовним від одного виконання програми до іншого виконання тієї самої програми.

  • Якщо два об'єкти рівні за методом рівних s (Obj ect), то виклик методу hashCode на кожному з двох об'єктів повинен давати однаковий цілий результат.

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


5

Якщо масиви у вашому розміщеному хеш-коді є байтами, то, швидше за все, ви отримаєте багато дублікатів.

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


4

HashMap має початкову потужність, а продуктивність HashMap дуже дуже залежить від hashCode, який виробляє основні об'єкти.

Спробуйте виправити обох.


4

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

Приклад: Ключі: 1,2,3, .... n 28 карт по 1 мільйон кожна. Індексна карта: 1-1,000,000 -> Map1 1,000,000-2,000,000 -> Map2

Отже, ви будете робити два пошуки, але набір ключів складе 1 000 000 проти 28 000 000. Ви також можете легко зробити це з допомогою жалячих візерунків.

Якщо ключі є абсолютно випадковими, це не спрацює


1
Навіть якщо ключі випадкові, ви можете скористатися (key.hashCode ()% 28), щоб вибрати карту, де зберігати це значення ключа.
Juha Syrjälä

4

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

У цьому випадку і заповнення, і отримання значень із сховища даних, звичайно, буде набагато швидше, якщо ви використовуєте масив замість HashMap та індексуєте його від 0 до 25989599.


Це дуже гарна ідея, і я насправді роблю це для іншого питання зберігання даних, що містить 1,2 мільярда елементів. У цьому випадку я хотів зробити простий вихід і використати попередньо створену структуру даних :)
наш

4

Я спізнююсь тут, але пара коментує великі карти:

  1. Як детально обговорювалося в інших публікаціях, з хорошим хеш-кодом (), 26М записів на карті - це не велика справа.
  2. Однак потенційно прихованою проблемою тут є вплив ГК на гігантські карти.

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

Кожен запис у Java HashMap потребує трьох об’єктів: ключ, значення та запис, який зв'язує їх між собою. Отже, 26М записів на карті означає 26М * 3 == 78М об’єктів. Це добре, поки ви не отримаєте повний GC. Тоді у вас є проблема паузи у світі. GC розгляне кожен із об'єктів 78M і визначить, що вони всі живі. 78M + об'єктів - це просто багато об’єктів, які потрібно подивитися. Якщо ваш додаток може терпіти періодичні довгі (можливо, багато секунд) паузи, проблем не виникає. Якщо ви намагаєтеся досягти гарантій затримки, у вас може виникнути головна проблема (звичайно, якщо ви хочете гарантувати затримку, Java не є платформою для вибору :)) Якщо значення на ваших картах швидко збиваються, ви можете закінчитися частими повними збірками що сильно поєднує проблему.

Я не знаю чудового рішення цього питання. Ідеї:

  • Іноді можна настроїти розміри GC та купи, щоб "переважно" запобігти повному вмісту GC.
  • Якщо вміст вашої картки сильно збивається, ви можете спробувати Javolution's FastMap - він може об'єднати об'єкти введення , що може знизити частоту повних колекцій
  • Ви можете створити власну карту impl і зробити явне управління пам’яттю на байт [] (тобто торгувати процесором для більш передбачуваної затримки шляхом серіалізації мільйонів об'єктів в один байт [] - так!)
  • Не використовуйте Java для цієї частини - поговоріть із якоюсь передбачуваною БД в пам'яті через сокет
  • Сподіваємось, що новий колектор G1 допоможе (в основному стосується корпусу з високим розміром)

Просто деякі думки від того, хто багато часу проводив з гігантськими картами на Яві.



3

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

З мого експерименту (студентський проект у 2009 році):

  • Я створив Червоне чорне дерево на 100 000 вузлів від 1 до 100 000. На це пішло 785,68 секунди (13 хвилин). І мені не вдалося створити RBTree на 1 мільйон вузлів (як, наприклад, ваші результати з HashMap).
  • Використовуючи "Prime Tree", моя структура алгоритму. Я міг би створити дерево / карту на 10 мільйонів вузлів протягом 21,29 секунд (оперативна пам'ять: 1,97 Гбіт). Ключова вартість пошуку - О (1).

Примітка: "Prime Tree" найкраще працює на "безперервних клавішах" від 1 до 10 мільйонів. Для роботи з ключами на зразок HashMap нам потрібні деякі неповнолітні налаштування.


Отже, що таке #PrimeTree? Коротше кажучи, це структура даних дерева, як Бінарне дерево, з числами гілок є простими числами (замість "2" -бінарними).


Чи можете ви поділитися посиланням чи реалізацією?
Бендж



1

Чи вирішили ви використовувати вбудовану базу даних для цього. Подивіться на Берклі DB . Це відкритий код, зараз належить Oracle.

Він зберігає все як пару Key-> Value, це НЕ RDBMS. і воно має на меті бути швидким.


2
Berkeley DB ніде не так швидко для цієї кількості записів через накладні витрати на серіалізацію / введення; це ніколи не може бути швидшим, ніж хеш-карта, і ОП не піклується про наполегливість. Ваша пропозиція не є вдалою.
oxbow_lakes

1

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

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

Якщо це не допомагає, як щодо використання чогось типу EHCache або memcached ? Так, вони є продуктами для кешування, але ви можете налаштувати їх так, що вони матимуть достатню ємність і ніколи не вилучатимуть ніяких значень із сховища кешу.

Іншим варіантом буде деякий двигун бази даних, який легший за вагу, ніж повний SQL RDBMS. Можливо, щось на кшталт Berkeley DB .

Зауважте, що я особисто не маю досвіду роботи цих продуктів, але вони можуть бути варті того, щоб спробувати.


1

Ви можете спробувати кешувати обчислений хеш-код ключовим об'єктом.

Щось на зразок цього:

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

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

Редагувати: Здається, що кешування має значення коду, не варто, коли ви додаєте кожну клавішу лише один раз до карти. В іншій ситуації це може бути корисним.


Як зазначено нижче, в HashMap немає перерахунку хеш-кодів об'єктів, коли він змінюється, тому це нічого не отримує.
delfuego

1

Інший плакат уже вказував, що ваша реалізація хеш-коду призведе до великої кількості зіткнень через те, що ви додаєте значення разом. Я готовий бути таким, що якщо ви подивитесь на об'єкт 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;

@kdgregory: Я відповів про те, що "більше зіткнень передбачає більше роботи" десь ще :)
OscarRyz

1

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

Якщо вам потрібно додатково оптимізувати:

За вашим описом, є лише (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;  

Я думаю, це без зіткнень. Доведення цього залишається як вправа для математично схильного читача.


1

In Effective Java: Посібник з мов програмування (серія Java)

У розділі 3 ви можете знайти хороші правила, яких слід дотримуватися під час обчислення hashCode ().

Спеціально:

Якщо поле - це масив, розглядайте його так, ніби кожен елемент є окремим полем. Тобто обчисліть хеш-код для кожного значущого елемента, застосовуючи ці правила рекурсивно, та об'єднайте ці значення за крок 2.b. Якщо кожен елемент поля масиву є значущим, ви можете використовувати один з методів Arrays.hashCode, доданий у версії 1.5.


0

Виділіть на початку велику карту. Якщо ви знаєте, що в ньому буде 26 мільйонів записів, і у вас є пам'ять, зробіть це new HashMap(30000000).

Ви впевнені, у вас достатньо пам'яті для 26 мільйонів записів з 26 мільйонами ключів і значень? Це мені звучить як багато пам’яті. Ви впевнені, що прибирання сміття все ще працює на ваших позначках від 2 до 3 мільйонів? Я міг уявити це як вузьке місце.


2
О, інша річ. Ваші хеш-коди повинні бути розподілені рівномірно, щоб уникнути великих пов'язаних списків на одних позиціях на карті.
ReneS

0

Ви можете спробувати дві речі:

  • Зробіть так, щоб ваш hashCodeметод повернув щось більш просте та ефективне, наприклад, послідовне введення

  • Ініціалізуйте свою карту як:

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

Ці дві дії надзвичайно скоротять кількість повторного переробки конструкції, і це досить легко перевірити.

Якщо це не працює, розгляньте можливість використання іншого RDBMS для зберігання даних.

EDIT

Дивно, що встановлення початкової ємності знижує продуктивність у вашому випадку.

Дивіться з javadocs :

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

Я зробив мікробік (який не є остаточним, але принаймні доводить це)

$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методом як "сферою можливостей";)

EDIT

Чи не HashMap

Відповідно до останнього видання.

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

Я створив клас, реалізуючи ваше те саме hashCode

Цей хеш-код дає мільйони зіткнень, тоді записи в HashMap різко скорочуються.

Я проходжу з 21, 16 в попередньому тесті до 10 і 8 с. Причина полягає в тому, що хеш-код провокує велику кількість зіткнень, і ви не зберігаєте об'єкти 26М, які ви думаєте, але набагато значніші менші числа (я б сказав, приблизно 20 к)

Проблеми НЕ ХАШМАП є десь ще у вашому коді.

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

Ось моя реалізація вашого класу.

зауважте, що я не використовував діапазон 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

3
Оскар, як вказувалося в іншому місці вище (у відповідь на ваші коментарі), ви, здається, припускаєте, що більше зіткнень - ДОБРЕ; це дуже НЕ добре. Зіткнення означає, що слот для заданого хеша переходить від вмісту одного запису до списку записів, і цей список потрібно шукати / проходити кожен раз, коли доступ до слота.
delfuego

@delfuego: Не дуже, це відбувається лише тоді, коли у вас зіткнення з використанням різних класів, але для одного класу використовується той самий запис;)
OscarRyz

2
@Oscar - дивіться мою відповідь до вас з відповіддю MAK. HashMap підтримує зв'язаний список записів у кожному хеш-відрі та здійснює прогулянки, в яких перелік викликів дорівнює () для кожного елемента. Клас об'єкта не має нічого спільного з ним (крім короткого замикання на рівності ()).
kdgregory

1
@Oscar - Читаючи свою відповідь, здається, ви припускаєте, що рівняння () повернеться істинним, якщо хеш-коди однакові. Це не є частиною договору рівності / хеш-коду. Якщо я неправильно зрозумів, ігноруйте цей коментар.
kdgregory

1
Дуже дякую за зусилля Оскар, але я думаю, що ви плутаєте ключові об'єкти, що дорівнюють рівним, і однаковий хеш-код Також в одному з ваших посилань на код ви використовуєте в якості ключових рівнів рядки, пам’ятайте, що рядки в Java незмінні. Я думаю, що ми обидва дізналися багато про хеширование сьогодні :)
наш


0

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


0

Використовувані популярні способи хешування не дуже хороші для великих наборів, і, як було зазначено вище, хеш, який використовується, є особливо поганим. Краще використовувати алгоритм хешування з високим змішуванням і покриттям, наприклад BuzHash (реалізація зразка за адресою http://www.java2s.com/Code/Java/Development-Class/AveryefficientjavahashalgorithmbasedontheBuzHashalgoritm.htm )

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