Есть ли преимущество использования map перед unordered_map в случае тривиальных ключей?

371

Недавний разговор о unordered_mapC ++ заставил меня понять, что я должен использовать unordered_mapв большинстве случаев, где я использовал mapраньше, из-за эффективности поиска ( амортизированный O (1) против O (log n) ). В большинстве случаев я использую карту, я использую intили std::stringкак тип ключа; следовательно, у меня нет проблем с определением хеш-функции. Чем больше я думал об этом, тем больше я осознавал, что не могу найти никаких причин использования std::mapover 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

Ответы:

399

Не забывайте, что mapдержит свои элементы в порядке. Если вы не можете отказаться от этого, очевидно, вы не можете использовать unordered_map.

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

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

Исходя из личного опыта, я обнаружил огромное улучшение производительности (измеряемой, конечно) при использовании unordered_mapвместо mapтаблицы поиска основной сущности.

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

GManNickG
источник
3
Еще одна вещь о большом (r) свойстве блока памяти unordered_map против map (или vector против списка), куча процесса по умолчанию (здесь речь идет о Windows) сериализуется. Выделение (небольших) блоков в больших количествах в многопоточном приложении очень дорого.
Рев
4
РА: Вы можете в некоторой степени контролировать это с помощью своего собственного типа распределителя в сочетании с любым контейнером, если считаете, что это имеет значение для любой конкретной программы.
9
Если вы знаете размер unordered_mapи резервируете его на старте - вы все равно платите штраф за многие вставки? Скажем, вы только один раз вставляете, когда строите таблицу поиска, а потом только читаете из нее.
Thomthom
3
@thomthom Насколько я могу судить, не должно быть никакого наказания с точки зрения производительности. Причиной снижения производительности является тот факт, что если массив станет слишком большим, он выполнит перефразировку всех элементов. Если вы вызываете резерв, он потенциально перефразирует существующие элементы, но если вы вызываете его в начале, то не должно быть никакого штрафа, по крайней мере, в соответствии с cplusplus.com/reference/unordered_map/unordered_map/reserve
Richard Fung
6
Я совершенно уверен, что в отношении памяти все наоборот. Предполагая коэффициент загрузки по умолчанию 1.0 для неупорядоченного контейнера: у вас есть один указатель на элемент для корзины и один указатель на элемент для следующего элемента в корзине, поэтому вы получите два указателя плюс данные на каждый элемент. Для упорядоченного контейнера, с другой стороны, типичная реализация дерева RB будет иметь: три указателя (левый / правый / родительский) плюс бит цвета, который из-за выравнивания занимает четвертое слово. Это четыре указателя плюс данные на каждый элемент.
Яков Галка
126

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

$ ./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 Я отредактировал вопрос, чтобы сослаться на ссылку обратного автомата .
Андре
Просто для того, чтобы другие заметили другие числа, кроме времени: эти тесты были выполнены с 4-байтовыми объектами / структурами данных, иначе говоря, int. Если вы храните что-то, что требует более интенсивного хеширования или больше (что делает операции копирования более тяжелыми), стандартная карта может быстро получить преимущество!
AlexGeorg
82

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

Есть несколько усложняющих факторов, которые нужно иметь в виду. Например, в std::map, вы сравниваете ключи, что означает, что вы только когда-либо просматриваете достаточно начала ключа, чтобы различить правую и левую ветви дерева. По моему опыту, почти единственный раз, когда вы смотрите на весь ключ, это если вы используете что-то вроде int, которое вы можете сравнить в одной инструкции. С более типичным типом ключа, таким как std :: string, вы часто сравниваете всего несколько символов или около того.

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

Во-вторых, хотя существует несколько методов изменения размера хеш-таблиц, большинство из них довольно медленные - до такой степени, что если поиск не происходит значительно чаще, чем вставки и удаления, std :: map часто будет быстрее, чем std::unordered_map.

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

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

Джерри Гроб
источник
2
Изменение размера хеша можно уменьшить с помощью dynamic hashingтехники, которая заключается в наличии переходного периода, когда каждый раз, когда вы вставляете элемент, вы также перефразируете kдругие элементы. Конечно, это означает, что во время перехода вы должны искать 2 разных таблицы ...
Matthieu M.
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: верный момент - жизнь была проще, когда я использовал свою собственную реализацию - по крайней мере, я знал, где она отстой:>
Корнел Киселевич
25

Существенные различия, которые не были должным образом упомянуты здесь:

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

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

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

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


источник
@ Роджер, знаете ли вы приблизительное количество элементов, на которых отображается unordered_map? Возможно, я все равно напишу тест для него ... (+1)
Корнел Киселевич
1
@Kornel: это не очень много; мои тесты были с около 10000 элементов. Если нам нужен действительно точный график, вы можете посмотреть на реализацию mapи одну из них unordered_map, с определенной платформой и определенным размером кэша, и провести комплексный анализ. : P
GManNickG
Зависит от деталей реализации, параметров настройки во время компиляции (легко поддерживается, если вы пишете свою собственную реализацию) и даже от конкретной машины, используемой для тестов. Как и для других контейнеров, комитет только устанавливает широкие требования.
13

Причины были приведены в других ответах; здесь другое.

Операции std :: map (сбалансированное двоичное дерево) амортизируются O (log n) и наихудшим O (log n). Операции std :: unordered_map (hash table) амортизируются O (1) и наихудшим O (n).

На практике это проявляется в том, что хэш-таблица «икает» время от времени с помощью операции O (n), что может или не может быть тем, что ваше приложение может терпеть. Если это не терпит, вы бы предпочли std :: map вместо 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как отдельный тип.

Представление

Ниже два графика должны говорить сами за себя ( источник ):

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

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

Шиталь шах
источник
Интересные данные; сколько платформ вы включили в свои тесты?
Тоби Спейт
1
почему я должен использовать std :: map для маленькой таблицы при выполнении большого количества запросов, поскольку std :: unordered_map всегда работает лучше, чем std :: map, согласно 2 изображениям, которые вы разместили здесь?
Рикки
График показывает производительность для 0,13M или более элементов. Если у вас есть небольшие (может быть <100) элементы, то O (log n) может стать меньше, чем неупорядоченная карта.
Shital Shah
10

Недавно я сделал тест, который делает 50000 слияния и сортировки. Это означает, что если строковые ключи совпадают, объедините байтовую строку. И окончательный вывод должен быть отсортирован. Так что это включает поиск каждой вставки.

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

Мы должны дважды подумать, прежде чем использовать map. Если вам нужно только отсортировать данные в конечном результате вашей программы, лучше использовать гибридное решение.

Wendong
источник
0

Небольшое дополнение ко всему вышеперечисленному:

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

Денис Саблуков
источник
-1

От: http://www.cplusplus.com/reference/map/map/

«Внутренне элементы в карте всегда сортируются по ее ключу в соответствии с определенным строгим критерием слабого упорядочения, указанным его внутренним объектом сравнения (типа« Сравнить »).

Контейнеры map обычно медленнее, чем контейнеры unordered_map, для доступа к отдельным элементам по их ключу, но они допускают прямую итерацию для подмножеств в зависимости от их порядка ».

Кунал Бансал
источник