如果使用的是普通键,使用地图而不使用unordered_map有什么优势吗?


371

最近一次有关unordered_mapC ++的讨论使我意识到,由于查找效率高(摊销O(1)O(log n)),我应该unordered_mapmap以前使用过的大多数情况下使用。大多数情况下,我使用地图,或者使用或作为密钥类型。因此,我对散列函数的定义没有任何问题。我想得越多,就越发意识到,对于简单类型的键,我找不到使用over的任何原因-我看了一下接口,却没有发现任何原因会影响我的代码的重大差异。intstd::stringstd::mapstd::unordered_map

因此,问题:是否有使用任何真正的原因std::mapstd::unordered_map简单类型等的情况下intstd::string

我是从严格的编程角度询问的-我知道它还没有完全被认为是标准的,并且可能会带来移植方面的问题。

另外,我希望正确的答案之一可能是由于开销较小(对于较小的数据集,效率更高)”(是吗?),因此,我想将问题限制在以下情况:键是不平凡的(> 1,024)。

编辑: h,我忘了明显的东西(感谢GMan!)-是的,当然,地图是有序的-我知道,并且正在寻找其他原因。


22
我喜欢在采访中问这个问题:“什么时候快速排序比气泡排序更好?” 该问题的答案提供了对复杂性理论的实际应用的洞察力,而不仅仅是简单的黑白陈述,例如O(1)优于O(n)或O(k)等同于O(logn)等。 ..

42
@Beh,我想您的意思是“何时气泡分类比快速分类更好”:P
Kornel Kisielewicz

2
智能指针会成为一个琐碎的钥匙吗?
thomthom 2013年

在以下情况中,使用地图是有利的一种情况:stackoverflow.com/questions/51964419/…–
anilbey

Answers:


398

别忘了map保持其元素有序。如果您不能放弃它,显然您将无法使用unordered_map

要记住的另一件事是unordered_map通常使用更多的内存。map只是有一些管家指针,以及每个对象的内存。相反,unordered_map有一个大数组(在某些实现中这些数组可能会很大),然后为每个对象增加内存。如果您需要内存感知,map应该证明它更好,因为它缺少大型数组。

因此,如果您需要纯粹的查找查询,我想这unordered_map是要走的路。但是总会有一些折衷,如果您负担不起,那么就无法使用。

仅凭个人经验,我发现使用unordered_map而不是使用map主要实体查询表时,性能有了很大的提高(当然是衡量的)。

另一方面,我发现重复插入和删除元素要慢得多。对于相对静态的元素集合而言,这非常好,但是如果您要进行大量的插入和删除操作,则哈希+分桶似乎会加起来。(请注意,这是多次迭代。)


3
关于unordered_map与map(或vector与list)的large(r)内存块属性的另一件事是,默认进程堆(在此与Windows进行了交谈)已被序列化。在多线程应用程序中大量分配(小)块非常昂贵。

4
RA:如果您认为它对任何特定程序都很重要,则可以通过将自己的分配器类型与任何容器结合使用来进行某种程度的控制。

9
如果您知道的大小unordered_map并在开始时保留该大小-您是否仍要为多次插入付出代价?假设您在建立查找表时只插入了一次,然后再从中读取一次。
thomthom 2013年

3
据我所知,@ thomthom在性能方面不应受到任何惩罚。性能受到打击的原因是由于以下事实:如果数组变得太大,它将对所有元素进行重新哈希处理。如果您调用reserve,它将有可能重新哈希现有元素,但是如果您一开始就调用它,那么至少应根据cplusplus.com/reference/unordered_map/unordered_map/reserve
Richard Fung

6
我很确定在内存方面是相反的。假设无序容器的默认加载因子为1.0:对于存储区,每个元素有一个指针,对于下一元素存储区,每个元素有一个指针,因此最终每个元素有两个指针和数据。另一方面,对于有序容器,典型的RB树实现将具有:三个指针(左/右/父)和一个颜色位,该颜色位由于对齐而需要第四个字。那是每个元素四个指针加上数据。
Yakov Galka'9

126

如果您想比较自己std::mapstd::unordered_map实施的速度,可以使用Google的sparsehashash项目,该项目具有time_hash_map程序来对它们进行计时。例如,在x86_64 Linux系统上使用gcc 4.4.2

$ ./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
看起来在大多数操作上,无序地图击败了地图。插入事件...
Michael IV

7
sparsehash不再存在。它已被删除或删除。
User9102d82

1
@ User9102d82我已经编辑了问题,以引用一个waybackmachine链接
andreee

只是为了确保其他人也注意到时间以外的其他数字:这些测试是使用4字节对象/数据结构(也称为int)完成的。如果存储的东西需要更大的哈希值或更大的哈希值(使复制操作更繁重),则标准映射可能很快就具有优势!
AlexGeorg

82

我回想了GMan提出的大致相同的观点:根据使用类型的不同,std::map它可能(并且通常)比std::tr1::unordered_map(使用VS 2008 SP1中包含的实现)更快(并且通常更快)。

要记住一些复杂的因素。例如,在中std::map,您正在比较键,这意味着您仅查看键开头的足够部分就可以区分树的左右子分支。根据我的经验,几乎唯一一次查看整个键的情况是,如果使用的是int之类的东西,则可以在一条指令中进行比较。使用更典型的键类型(例如std :: string),您通常只比较几个字符左右。

相比之下,体面的哈希函数始终查看整个键。IOW,即使表查找是恒定的复杂度,哈希本身也具有大致线性的复杂度(尽管在键的长度上,而不是项目数上)。使用长字符串作为键,an 甚至std::map可以在开始搜索之前完成搜索。unordered_map

其次,虽然有调整哈希表的几种方法,其中大部分是相当缓慢的-来,除非是查找点大大高于插入和缺失更频繁,性病::地图通常会比快std::unordered_map

当然,正如我在上一个问题的评论中提到的那样,您还可以使用树形表。这具有优点和缺点。一方面,它将最坏的情况限制为一棵树。它还允许快速插入和删除,因为(至少在完成后)我使用了固定大小的表。消除所有表的大小调整,可以使哈希表保持简单得多,并且通常更快。

另一点:散列和基于树的映射的要求不同。显然,散列需要散列函数和相等性比较,其中有序映射需要小于的比较。当然,我提到的混合动力车都需要两者。当然,对于使用字符串作为键的常见情况,这并不是真正的问题,但是某些类型的键比哈希更适合排序(反之亦然)。


2
散列调整大小可以通过dynamic hashing技术来降低,这包括一个过渡期,在该过渡期中,每次插入项目时,您还需要重新散列k其他项目。当然,这意味着在过渡期间您必须搜索2个不同的表...
Matthieu M.

2
“使用长字符串作为键,std :: map可能会在unordered_map甚至开始搜索之前完成搜索。” -如果密钥不在集合中。如果存在,那么当然需要比较全长以确认匹配。但是同样unordered_map需要通过完全比较来确认哈希匹配,因此这全都取决于您要对比的查找过程的哪一部分。
史蒂夫·杰索普

2
您通常可以根据数据知识替换哈希函数。例如,如果你的长串变化多在过去的20个字节比第一个100,刚凑过去的20
埃里克Aronesty

56

@Jerry Coffin的回答使我很感兴趣,它暗示有序映射在经过一些试验(可以从pastebin下载)后,在长字符串上表现出性能提高,我发现这似乎只对集合成立对于随机字符串,当使用排序字典(包含有大量前缀重叠的单词)初始化地图时,此规则将失效,大概是因为检索值所需的树深增加了。结果如下所示,第一个数字列是插入时间,第二个是获取时间。

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))。
Andreas Magnusson

30

我要指出的是...有许多种unordered_map

在哈希图上查找Wikipedia文章。根据所使用的实现方式,在查找,插入和删除方面的特性可能会有很大差异。

这就是添加unordered_mapSTL时最让我担心的地方:他们将不得不选择特定的实现,因为我怀疑它们会Policy沿途走下去,因此我们将停留在通常用途的实现上,而对于其他情况...

例如,某些哈希图具有线性重新哈希,其中不是一次重新哈希整个哈希图,而是在每次插入时重新哈希一部分,这有助于摊销成本。

另一个例子:一些哈希映射使用一个简单的节点列表来存储桶,其他哈希映射使用一个映射,其他哈希映射不使用节点,但是找到最近的插槽,最后一些哈希使用节点列表,但是重新排序以便最后访问的元素位于最前面(就像缓存一样)。

因此,目前,我倾向于使用std::map或也许是loki::AssocVector(对于冻结的数据集)。

不要误会我的意思,我想使用,std::unordered_map将来我会使用,但是当您想到实现这种容器的所有方式以及所产生的各种性能时,很难“信任”这种容器的可移植性这个的。


17
+1:有效的点-生活更轻松,当我用我自己的实现-至少我知道在那里它吸:>
科内尔·基利尔威奇斯

25

此处尚未充分提及的重大差异:

  • map使所有元素的迭代器保持稳定,在C ++ 17中,您甚至可以将元素从一个元素移到另一个元素,map而不会使对它们的迭代器无效(并且如果正确实现且没有任何潜在的分配)。
  • map 单个操作的时间通常更为一致,因为它们永远不需要大量分配。
  • unordered_map如果使用std::hashlibstdc ++中实现的方式进行操作,则在受到不受信任的输入的情况下很容易受到DoS的攻击(它使用具有恒定种子的MurmurHash2-并不是播种确实有帮助,请参见https://emboss.github.io/blog/2012/12/14/重新加载杂乱无章的哈希杂波/)。
  • 被订购可以实现有效的范围搜索,例如,对键≥42的所有元素进行迭代。

14

哈希表具有比常见的映射实现更高的常量,这对于小型容器而言非常重要。最大大小是10、100,甚至1000甚至更多?常数与以往相同,但是O(log n)接近O(k)。(记住对数的复杂性仍然是真的不错。)

什么才是一个好的哈希函数,取决于您数据的特征。因此,如果我不打算查看自定义哈希函数(但是肯定可以稍后改变主意,因为我在所有内容附近都键入def damn),即使选择默认值来对许多数据源表现良好,我也会发现有序映射的本质足以在开始时提供帮助,在这种情况下,我仍然默认使用映射而不是哈希表。

加上这种方式,您甚至不必考虑为其他(通常是UDT)类型编写哈希函数,而只需编写op <(无论如何都需要)。


@Roger,您知道unordered_map最好映射的元素的大概数量吗?无论如何,我可能还是会为此做一个测试...(+1)
Kornel Kisielewicz 2010年

1
@Kornel:不需要很多;我的测试包含大约10,000个元素。如果我们希望有一个真正准确的图表,你可以看看的实现map和一个unordered_map与某些平台和某些高速缓存大小,并做了复杂的分析。:P
GManNickG'2

取决于实现细节,编译时调整参数(如果要编写自己的实现,则很容易获得支持),甚至取决于用于测试的特定机器。就像其他容器一样,委员会仅设定了广泛的要求。

13

其他答案中给出了原因;这是另一个。

std :: map(平衡二叉树)操作分期偿还O(log n)和最坏情况O(log n)。std :: unordered_map(哈希表)操作分摊O(1)和最坏情况O(n)。

在实践中如何发挥作用是散列表偶尔会通过O(n)操作“打ic”,这可能是您的应用程序可以容忍的,也可能不是。如果它不能忍受,您更喜欢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成为单独的类型。

性能

以下两个图表应能说明问题(来源):

在此处输入图片说明

在此处输入图片说明


有趣的数据;您在测试中包括了多少个平台?
Toby Speight '18

1
为什么在执行大量查询时我应该在小表上使用std :: map,因为根据您在此处发布的2张图片,std :: unordered_map总是比std :: map更好?
ricky

该图显示了0.13M或更多元素的性能。如果元素较小(可能小于100),则O(log n)可能会小于无序映射。
Shital Shah

10

我最近进行了一项测试,该测试使50000合并和排序。这意味着如果字符串键相同,则合并字节字符串。最后的输出应该排序。因此,这包括对每个插入的查找。

对于map实施,需要200毫秒才能完成工作。对于unordered_map+ mapunordered_map插入需要70毫秒,插入需要80毫秒map。因此,混合实施的速度提高了50毫秒。

在使用之前,我们应该三思map。如果只需要在程序的最终结果中对数据进行排序,则混合解决方案可能会更好。



By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.