Чи є якась перевага використання карти над unordered_map у разі тривіальних ключів?


371

Нещодавно розмова про unordered_mapC ++ дала мені зрозуміти, що я повинен використовуватись unordered_mapу більшості випадків, коли я використовував mapраніше, через ефективність пошуку ( амортизований O (1) проти O (log n) ). У більшості випадків я використовую карту, я використовую intабо std::stringключовий тип; отже, у мене немає проблем із визначенням хеш-функції. Чим більше я думав про це, тим більше я зрозумів, що я не можу знайти жодної причини використання std::mapнадмірника std::unordered_mapу випадку клавіш з простими типами - я переглянув інтерфейси і не знайшов жодного суттєві відмінності, які вплинули б на мій код.

Звідси питання: чи є реальна причина для використання std::mapбільш ніж std::unordered_mapв разі простих типів , як intі std::string?

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

Крім того, я очікую, що одна з правильних відповідей може бути "ефективніше для менших наборів даних" через менші накладні витрати (це правда?) - отже, я хотів би обмежити питання випадками, коли кількість клавіші нетривіальні (> 1 024).

Редагувати: так , я забув очевидне (спасибі GMan!) - так, карти замовлені звичайно - я це знаю, і шукаю інші причини.


22
Мені подобається задавати це питання в інтерв'ю: "Коли швидше сортувати краще, ніж сортувати бульбашки?" Відповідь на питання дає розуміння практичного застосування теорії складності, а не просто звичайні чорно-білі висловлювання, такі як O (1), краще, ніж O (n) або O (k) еквівалентно O (logn) тощо. ..

42
@ Beh, я думаю, ти мав на увазі "коли бульбашка сортувати краще, ніж швидко сортувати": P
Kornel Kisielewicz

2
Чи буде розумний вказівник тривіальним ключем?
thomthom

Ось один з випадків , в яких карта є виграшним: stackoverflow.com/questions/51964419 / ...
anilbey

Відповіді:


398

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

Щось ще слід пам’ятати, це те, що unordered_mapзазвичай використовується більше пам’яті. mapпросто має кілька домашніх покажчиків та пам'ять для кожного об’єкта. Навпаки, unordered_mapє великий масив (вони можуть бути досить великими в деяких реалізаціях), а потім додаткова пам'ять для кожного об'єкта. Якщо вам потрібно мати пам'ять, mapслід довести кращі результати, оскільки не вистачає великого масиву.

Отже, якщо вам потрібно чисте пошуку, я б сказав, що unordered_mapце шлях. Але завжди є компроміси, і якщо ти не можеш їх дозволити, то ти не можеш їх використати.

Тільки з особистого досвіду я виявив величезне поліпшення продуктивності (вимірюється, звичайно), коли використовував unordered_mapзамість mapосновної таблиці огляду сутності.

З іншого боку, я виявив, що це було набагато повільніше при багаторазовому вставленні та видаленні елементів. Це чудово підходить для відносно статичної колекції елементів, але якщо ви робите багато вставок і видалень, хеш + букетинг, схоже, додається. (Зауважте, це було над багатьма ітераціями.)


3
Ще одне про велике (r) властивість блоку пам'яті unorряд_map vs. map (або вектор vs list), серія процесів за замовчуванням (тут йде мова про Windows). Виділення (невеликих) блоків у великих кількостях у багатопотоковому застосуванні дуже дороге.
ROAR

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

9
Якщо ви знаєте розмір unordered_mapта резерв, який на початку - ви все одно платите штраф за багато вкладок? Скажімо, ви вставляєте лише один раз, коли ви створювали таблицю пошуку, а потім читаєте лише з неї.
thomthom

3
@thomthom Наскільки я можу сказати, не повинно бути штрафу з точки зору продуктивності. Причина, по якій продуктивність вражає, пов’язана з тим, що якщо масив зросте занадто великим, він здійснить повторне перетворення всіх елементів. Якщо ви викликаєте резерв, він потенційно може повторно переробити
Річард Фунг

6
Я повністю впевнений, що в пам’яті все навпаки. Якщо припустити коефіцієнт завантаження за замовчуванням для невпорядкованого контейнера: у вас є один вказівник на елемент для відра і один вказівник на елемент для наступного елемента-у-відрі, тому в кінцевому підсумку ви маєте два покажчики плюс дані на кожен елемент. Для впорядкованого контейнера, з іншого боку, типова реалізація дерева дерев RB матиме: три вказівники (лівий / правий / батьківський) плюс кольоровий біт, який через вирівнювання приймає четверте слово. Це чотири покажчики плюс дані на кожен елемент.
Яків Галка

126

Якщо ви хочете порівняти швидкість ваших std::mapі std::unordered_mapреалізацій, ви можете використовувати Google, sparsehash проект , який має програму time_hash_map часу їм. Наприклад, з gcc 4.4.2 в системі x86_64 Linux

$ ./time_hash_map
TR1 UNORDERED_MAP (4 byte objects, 10000000 iterations):
map_grow              126.1 ns  (27427396 hashes, 40000000 copies)  290.9 MB
map_predict/grow       67.4 ns  (10000000 hashes, 40000000 copies)  232.8 MB
map_replace            22.3 ns  (37427396 hashes, 40000000 copies)
map_fetch              16.3 ns  (37427396 hashes, 40000000 copies)
map_fetch_empty         9.8 ns  (10000000 hashes,        0 copies)
map_remove             49.1 ns  (37427396 hashes, 40000000 copies)
map_toggle             86.1 ns  (20000000 hashes, 40000000 copies)

STANDARD MAP (4 byte objects, 10000000 iterations):
map_grow              225.3 ns  (       0 hashes, 20000000 copies)  462.4 MB
map_predict/grow      225.1 ns  (       0 hashes, 20000000 copies)  462.6 MB
map_replace           151.2 ns  (       0 hashes, 20000000 copies)
map_fetch             156.0 ns  (       0 hashes, 20000000 copies)
map_fetch_empty         1.4 ns  (       0 hashes,        0 copies)
map_remove            141.0 ns  (       0 hashes, 20000000 copies)
map_toggle             67.3 ns  (       0 hashes, 20000000 copies)

2
Схоже, що не упорядкована карта б’є карту на більшості операцій.
Михайло IV

7
sparsehash більше не існує. це було видалено або знято.
User9102d82

1
@ User9102d82 Я відредагував це запитання, щоб вказати на посилання зворотної машини .
andreee

Просто для того, щоб інші, крім часу, помітили й інші числа: Ці тести були виконані за допомогою 4-х байтних об’єктів / структур даних, так само як int. Якщо ви зберігаєте щось, що вимагає більшого хешування або більше (що робить операції копіювання важчішими), стандартна карта може швидко отримати перевагу!
AlexGeorg

82

Я б нагадував приблизно те саме, що зробив GMan: залежно від типу використання, std::mapможе бути (і часто є) швидшим, ніж std::tr1::unordered_map(використовуючи реалізацію, включену в VS 2008 SP1).

Майте на увазі кілька складних факторів. Наприклад, у програмі std::mapви порівнюєте ключі, а це означає, що ви коли-небудь переглядаєте достатньо початку ключа, щоб розрізнити праву та ліву підгалузі дерева. На мій досвід, майже єдиний раз, коли ви переглядаєте цілий ключ, це якщо ви використовуєте щось на зразок int, яке ви можете порівняти в одній інструкції. З більш типовим типом клавіш, таким як std :: string, ви часто порівнюєте лише кілька символів.

Пристойна хеш-функція, навпаки, завжди дивиться на весь ключ. IOW, навіть якщо пошук таблиці є постійною складністю, сам хеш має приблизно лінійну складність (хоча на довжину ключа, а не на кількість елементів). З довгими рядками як клавішами, std::mapможливо, закінчення пошуку ще до того, як unordered_mapнавіть почати його пошук.

По-друге, хоча існує кілька методів зміни розміру хеш-таблиць, більшість з них досить повільні - до того моменту, якщо пошуки значно частіші, ніж вставки та видалення, std :: map часто буде швидше, ніж std::unordered_map.

Звичайно, як я вже згадував у коментарі до вашого попереднього питання, ви також можете використовувати таблицю дерев. Це має як переваги, так і недоліки. З одного боку, він обмежує найгірший випадок із дерева. Це також дозволяє швидко вставляти та видаляти, оскільки (принаймні, коли я це робив) я використовував таблицю фіксованого розміру. Усунення всіх розмірів таблиць дозволяє вам зробити хеш-таблицю набагато простішою і, як правило, швидшою.

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


2
Змінення розміру хешу може бути зменшено dynamic hashingтехнікою, яка полягає в перехідному періоді, коли кожен раз, коли ви вставляєте предмет, ви також переробляєте kінші елементи. Звичайно, це означає, що під час переходу вам доведеться шукати 2 різні таблиці ...
Матьє М.

2
"З довгими рядками як ключами, std :: map може закінчити пошук, перш ніж unordered_map навіть розпочне його пошук." - якщо ключ не присутній у колекції. Якщо він присутній, то, звичайно, потрібно порівнювати повну довжину для підтвердження відповідності. Але так само unordered_mapпотрібно підтвердити відповідність хешу з повним порівнянням, тому все залежить від того, які частини процесу пошуку ви контрастуєте.
Стів Джессоп

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

56

Мене заінтригувала відповідь від @Jerry Coffin, яка припустила, що впорядкована карта буде демонструвати підвищення продуктивності на довгих рядках, після деякого експерименту (який можна завантажити з pastebin ) я виявив, що це, здається, справедливо для колекцій випадкових рядків, коли карта ініціалізована сортованим словником (у якому містяться слова зі значною кількістю префіксу-перекриття), це правило руйнується, імовірно, через збільшення глибини дерева, необхідної для отримання значень. Результати показані нижче, 1-й стовпчик числа - це час вставки, 2-й - час отримання.

g++ -g -O3 --std=c++0x   -c -o stdtests.o stdtests.cpp
g++ -o stdtests stdtests.o
gmurphy@interloper:HashTests$ ./stdtests
# 1st number column is insert time, 2nd is fetch time
 ** Integer Keys ** 
 unordered:      137      15
   ordered:      168      81
 ** Random String Keys ** 
 unordered:       55      50
   ordered:       33      31
 ** Real Words Keys ** 
 unordered:      278      76
   ordered:      516     298

2
Дякую за тест. Щоб переконатися, що ми не вимірюємо шум, я змінив його для виконання кожної операції багато разів (і вставив лічильник замість 1 на карту). Я перемістив її по різній кількості клавіш (від 2 до 1000) і до ~ 100 клавіш на карті, std::mapяк правило, перевершує результати std::unordered_map, особливо для цілих клавіш, але ~ 100 клавіш, здається, він втрачає край і std::unordered_mapпочинає вигравати. Вставити вже упорядковану послідовність у a std::mapдуже погано, ви отримаєте його найгірший сценарій (O (N)).
Андреас Магнуссон

30

Я просто зазначу, що ... існує багато видів unordered_maps.

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

І саме це мене найбільше хвилює при додаванні unordered_mapдо STL: їм доведеться вибрати конкретну реалізацію, оскільки я сумніваюся, що вони пітимуть по Policyдорозі, і тому ми будемо застрягати з реалізацією для середнього використання і нічого для інші випадки ...

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

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

Тому на даний момент я, як правило, віддаю перевагу std::mapабо, можливо, loki::AssocVector(для заморожених наборів даних).

Не зрозумійте мене неправильно, я хотів би використати std::unordered_mapта, можливо, в майбутньому, але важко "довірити" портативність такого контейнера, коли ви думаєте про всі способи його застосування та різні результати, які дають результат це.


17
+1: дійсна точка - життя було легшим, коли я використовував власну реалізацію - принаймні я знав, куди це засмоктується:>
Kornel Kisielewicz

25

Істотні відмінності, які насправді не були адекватно зазначені тут:

  • mapзберігає ітератори для всіх елементів стабільними, в C ++ 17 ви навіть можете переміщати елементи від одного mapдо іншого, не відключаючи до них ітераторів (і якщо їх правильно реалізувати без будь-якого потенційного розподілу).
  • map Час виконання окремих операцій, як правило, більш послідовний, оскільки їм ніколи не потрібні великі асигнування.
  • unordered_mapвикористання std::hash, що реалізовано в libstdc ++, є вразливим для DoS, якщо його годують з ненадійним входом (він використовує MurmurHash2 з постійним насінням - не те, що насіннє дійсно допоможе, див. https://emboss.github.io/blog/2012/12/14/ break-murmur-hash-poplave-dos-reloaded / ).
  • Замовлення дозволяє ефективно шукати діапазон, наприклад, повторити всі елементи за допомогою клавіші ≥ 42.

14

Таблиці хешу мають вищі константи, ніж загальні реалізації карт, які стають значущими для невеликих контейнерів. Максимальний розмір - 10, 100, а може навіть 1000 чи більше? Константи такі ж, як завжди, але O (log n) близький до O (k). (Пам’ятайте, логарифмічна складність все ще дуже хороша.)

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

Крім того, вам навіть не потрібно думати про те, щоб написати хеш-функцію для інших типів (як правило, UDT), а просто написати op <(що ви хочете в будь-якому випадку).


@Roger, чи знаєш ти приблизну кількість елементів, на яких невпорядкована_мапа відображає карту? Я, мабуть, напишу тест на це, хоча б, все-таки ... (+1)
Kornel Kisielewicz

1
@Kornel: це займає не дуже багато; мої тести були з приблизно 10 000 елементами. Якщо ми хочемо дійсно точний графік, ви можете подивитися на реалізацію mapта один із unordered_map, із певною платформою та певним розміром кешу, та зробити складний аналіз. : P
GManNickG

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

13

Причини наведені в інших відповідях; ось ще одна.

std :: map (збалансоване бінарне дерево) операції амортизуються O (log n) і в гіршому випадку O (log n). std :: unordered_map (хеш-таблиця) операції амортизуються O (1) і в гіршому випадку O (n).

Як це відбувається на практиці, це те, що хеш-таблиця "ікониться" раз у раз за допомогою операції O (n), яка може бути, а може і не бути чимось, що ваша програма може терпіти. Якщо він не переносить цього, ви вважаєте за краще std :: map over std :: unordered_map.


12

Підсумок

Якщо припустити замовлення не важливо:

  • Якщо ви збираєтеся один раз зібрати велику таблицю і робити багато запитів, використовуйте std::unordered_map
  • Якщо ви збираєтеся будувати невелику таблицю (може бути менше 100 елементів) і робити багато запитів, використовуйте std::map. Це тому, що читається на ньому O(log n).
  • Якщо ви збираєтеся багато змінювати таблицю, то, можливо, std::map це хороший варіант.
  • Якщо ви сумніваєтесь, просто використовуйте std::unordered_map.

Історичний контекст

У більшості мов карта, що не впорядковується (так само словники на основі хешу), є карткою за замовчуванням, однак у C ++ ви отримуєте впорядковану карту як карту за замовчуванням. Як це сталося? Деякі люди помилково припускають, що комітет C ++ прийняв це рішення своєю неповторною мудрістю, але правда, на жаль, гірша за це.

Поширена думка, що C ++ узагальнено отримав упорядковану карту, оскільки не так вже й багато параметрів того, як вони можуть бути реалізовані. З іншого боку, на хеш-реалізаціях є багато речей. Тож, щоб уникнути неполадок у стандартизації, вони просто увійшли до замовленої карти. Близько 2005 року багато мов уже мали гарну реалізацію хеш-базованого впровадження, тому комітету було легше приймати нове std::unordered_map. У ідеальному світі std::mapбуло б не упорядковано, і ми мали б std::ordered_mapяк окремий тип.

Продуктивність

Нижче два графіки повинні говорити самі ( джерело ):

введіть тут опис зображення

введіть тут опис зображення


Цікаві дані; скільки платформ ви включили у свої тести?
Toby Speight

1
чому я повинен використовувати std :: map для невеликої таблиці, коли роблю багато запитів, оскільки std :: unordered_map завжди працює краще, ніж std :: map відповідно до двох зображень, які ви розмістили тут?
рикошет

Графік показує продуктивність для елементів 0,13M або більше. Якщо у вас є невеликі (може бути <100) елементів, то O (log n) може стати меншим, ніж не упорядкована карта.
Шітал Шах

10

Нещодавно я зробив тест, який робить 50000 злиття та сортування. Це означає, що якщо рядкові ключі однакові, об'єднайте рядок байтів. І кінцевий вихід слід сортувати. Отже, це включає пошук кожної вставки.

Для mapреалізації потрібно 200 мс, щоб закінчити роботу. Для unordered_map+ mapпотрібно 70 мс для unordered_mapвставки та 80 мс для mapвставки. Тож гібридна реалізація на 50 мс швидша.

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


0

Невелике доповнення до всього вищезазначеного:

Краще використовувати map, коли вам потрібно отримати елементи за діапазоном, оскільки вони сортуються, і ви можете просто перебирати їх від однієї межі до іншої.


-1

Від: http://www.cplusplus.com/reference/map/map/

Msgstr "" "Внутрішньо, елементи на карті завжди сортуються за її ключем за конкретним строгим критерієм слабкого впорядкування, вказаним його об'єктом внутрішнього порівняння (типу Порівняти).

Контейнери з картами, як правило, повільніше, ніж контейнери не упорядкованого_мапа для доступу до окремих елементів за їх ключем, але вони дозволяють пряму ітерацію для підмножини на основі їх порядку ".

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