A recent talk about unordered_map in C++ made me realize that I should use unordered_map for most cases where I used map before, because of the efficiency of lookup ( amortized O(1) vs. O(log n) ). Most times I use a map, I use either int or std::string as the key type; hence, I've got no problems with the definition of the hash function. The more I thought about it, the more I came to realize that I can't find any reason of using a std::map over a std::unordered_map in the case of keys with simple types -- I took a look at the interfaces, and didn't find any significant differences that would impact my code.

因此,问题是:是否有任何真正的理由使用std::map而不是std::unordered_map在简单类型的情况下,如int和std::string?

我是从严格的编程角度提出这个问题的——我知道它并不是完全标准的,而且它可能会给移植带来问题。

另外,我认为正确答案之一可能是“对于较小的数据集更有效”,因为开销更小(是真的吗?)——因此,我希望将这个问题限制在键数量非普通的情况下(>1 024)。

编辑:哦,我忘记了显而易见的(感谢GMan!)——是的,地图当然是有序的——我知道,我正在寻找其他原因。


当前回答

我大致同意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快。

当然,就像我在对你上一个问题的评论中提到的,你也可以使用树表。这既有优点也有缺点。一方面,它将最坏的情况限制在一棵树上。它还允许快速插入和删除,因为(至少当我这样做时)我使用了固定大小的表。消除所有的表大小调整可以让你的哈希表更简单,通常更快。

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

其他回答

哈希表具有比普通map实现更高的常量,这对于小型容器非常重要。最大尺寸是10个,100个,甚至1000个或更多?常数和以前一样,但是O(log n)接近O(k)。(记住,对数复杂度仍然很好。)

一个好的哈希函数取决于你的数据的特征;所以如果我不打算看一个自定义哈希函数(但肯定可以改变我的想法,而且很容易,因为我typedef几乎所有的东西),即使默认选择执行体面的许多数据源,我发现map的有序性质是足够的帮助,最初我仍然默认映射而不是哈希表在这种情况下。

另外,这样您甚至不必考虑为其他类型(通常是UDT)编写哈希函数,只需编写op<(无论如何您都想要)。

我认为这个问题已经部分回答了,因为没有提供关于以“int”类型作为键的性能的信息。我做了我自己的分析,我发现std::map在许多实际情况下使用整数作为键时可以胜过std::unordered_map(在速度上)。

整数测试

测试场景包括使用顺序键和随机键填充映射,以及长度为17的倍数[17:119]的字符串值。执行测试时,元素计数范围为[10:10000000],以10为幂。

Labels:

Map64: std::map<uint64_t,std::string>
Map32: std::map<uint32_t,std::string>
uMap64: std::unordered_map<uint64_t,std::string>
uMap32: std::unordered_map<uint32_t,std::string>

插入

Labels:

Sequencial Key Insert: maps were constructed with keys in the range [0-ElementCount]
Random Key Insert: maps were constructed with random keys in the full range of the type

关于插入的结论:

当映射大小小于10000个元素时,在std::map中插入展开键往往优于std::unordered_map。 在std::map中插入密集键在1000个元素下与std::unordered_map没有性能差异。 在所有其他情况下,std::unordered_map往往执行得更快。

查找

Labels:

Sequential Key - Seq. Search: Search is performed in the dense map (keys are sequential). All searched keys exists in the map.
Random Key - Rand. Search: Search is performed in the sparse map (keys are random). All searched keys exists in the map.

(label names can be miss leading, sorry about that)

关于查阅的结论:

当地图大小小于1000000个元素时,在std::map上的搜索往往略优于std::unordered_map。 密集std::map的搜索性能优于std::unordered_map

查询失败

Labels:

Sequential Key - Rand. Search: Search is performed in the dense map. Most keys do not exists in the map.
Random Key - Seq. Search: Search is performed in the sparse map. Most keys do not exists in the map.

(label names can be miss leading, sorry about that)

关于查找失败的结论:

在std::map中搜索缺失是一个很大的影响。

一般的结论

即使在需要速度时,std::map用于整数键在许多情况下仍然是更好的选择。举个实际的例子,我有一本字典 在这里查找从未失败,尽管键具有稀疏分布,但它将在与std::unordered_map相同的速度下执行得更差,因为我的元素计数低于1K。内存占用显著降低。

字符串的测试

为了供参考,我在这里给出了字符串[string]映射的计时。键字符串是由一个随机的uint64_t值形成的,值字符串是在其他测试中使用的相同。

Labels:

MapString: std::map<std::string,std::string>
uMapString: std::unordered_map<std::string,std::string>

评价平台

操作系统:Linux - OpenSuse风滚草

编译器:g++ (SUSE Linux) 11.2.1 20210816

CPU: Intel(R) Core(TM) i9-9900 CPU @ 3.10GHz

内存:64 gb

如果你用Visual Studio 2010编译项目-忘记字符串的unordered_map。 如果你使用更现代的Studio,比如2017 -那么unordered_map比ordered map快得多。

以上所有的小补充:

当你需要按范围获取元素时,最好使用map,因为它们是排序的,你可以从一个边界迭代到另一个边界。

I was intrigued by the answer from @Jerry Coffin, which suggested that the ordered map would exhibit performance increases on long strings, after some experimentation (which can be downloaded from pastebin), I've found that this seems to hold true only for collections of random strings, when the map is initialised with a sorted dictionary (which contain words with considerable amounts of prefix-overlap), this rule breaks down, presumably because of the increased tree depth necessary to retrieve value. The results are shown below, the 1st number column is insert time, 2nd is fetch time.

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