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

Недавний разговор о unordered_map в C++ заставил меня понять, что я должен использовать 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!) - да, карты, конечно, заказаны - я знаю это, и ищу по другим причинам.

Ответ 1

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

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

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

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

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

Ответ 2

Если вы хотите сравнить скорость ваших реализаций std::map и std::unordered_map, вы можете использовать проект Google sparsehash, в котором есть программа 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)

Ответ 3

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

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

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

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

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

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

Ответ 4

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

g++ -g -O3 --std=c++0x   -c -o stdtests.o stdtests.cpp
g++ -o stdtests stdtests.o
[email protected]: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

Ответ 5

Я бы просто отметил, что... существует много типов unordered_map s.

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

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

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

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

Итак, в данный момент я предпочитаю std::map или, возможно, <<24 > (для замороженных наборов данных).

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

Ответ 6

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

  • map сохраняет итераторы для всех элементов стабильными, в С++ 17 вы даже можете перемещать элементы из одного map в другой, не делая для них итераторы недействительными (и при правильной реализации без какого-либо потенциального распределения).
  • Времена map для отдельных операций, как правило, более согласованы, поскольку они никогда не требуют больших выделений.
  • unordered_map, использующий std::hash, как реализовано в libstdc++, уязвим для DoS, если подается с ненадежным вводом (он использует MurmurHash2 с постоянным начальным числом - не то, что начальное число действительно помогло бы, см. https://emboss.github.io/blog/2012/12/14/breaking-murmur-hash-flooding-dos-reloaded/).
  • ).При заказе можно эффективно выполнять поиск по дальности, например, переберите все элементы с ключом & ge; 42.

Ответ 7

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

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

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

Ответ 8

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

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

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

Ответ 9

Причины были даны в других ответах; вот еще один.

Операции

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

Как это происходит на практике, так это то, что хеш-таблица "икает" каждый раз с операцией O (n), которая может или не может быть чем-то, что ваше приложение может терпеть. Если он не может этого терпеть, вы предпочитаете std:: map over std:: unordered_map.

Ответ 10

Резюме

Предполагая, что заказ не важен:

  • Если вы собираетесь собрать большую таблицу один раз и выполнить много запросов, используйте 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 как отдельный тип.

Спектакль

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

enter image description here

enter image description here

Ответ 11

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

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

Ответ 12

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

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

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