【问题标题】:Is there any advantage of using map over unordered_map in case of trivial keys?在微不足道的键的情况下,使用 map 而不是 unordered_map 有什么优势吗?
【发布时间】:2011-01-12 21:51:22
【问题描述】:

最近一次关于 C++ 中 unordered_map 的讨论让我意识到,由于查找的效率(amortized O(1) em> 与 O(log n) )。大多数时候我使用地图,我使用intstd::string 作为键类型;因此,我对哈希函数的定义没有任何问题。我想得越多,我就越意识到在简单类型的键的情况下,我找不到使用std::map 而不是std::unordered_map 的任何理由——我看了一下接口,并且没有发现任何会影响我的代码的重大差异。

因此问题是:对于像intstd::string 这样的简单类型,是否有任何真正的理由使用std::map 而不是std::unordered_map

我是从严格的编程角度提问的——我知道它没有被完全认为是标准的,而且它可能会给移植带来问题。

另外,我希望正确答案之一可能是“它对较小的数据集更有效”,因为开销较小(是这样吗?)——因此我想将问题限制在密钥数量非平凡(> 1 024)的情况。

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

【问题讨论】:

  • 我喜欢在采访中问这个问题:“什么时候快速排序比冒泡排序更好?”该问题的答案提供了对复杂性理论实际应用的洞察,而不仅仅是简单的黑白陈述,例如 O(1) 优于 O(n) 或 O(k) 等效于 O(logn) 等。 ..
  • @Beh,我想你的意思是“什么时候冒泡排序比快速排序更好”:P
  • 智能指针会是一个微不足道的键吗?
  • 这里是地图是优势之一的情况之一:stackoverflow.com/questions/51964419/…
  • @Matthieu N. 在你的位置上,使用这种几乎永远不会有用并且不必要地让很多候选人感到尴尬的问题,我宁愿感到尴尬:/

标签: c++ performance dictionary unordered-map


【解决方案1】:

通过使用无序映射,您可以声明代码中的任何地方都不会依赖被排序的映射。在某些情况下,此附加上下文信息可能有助于了解此映射在程序中的实际使用方式。清晰度可能更重要,因为性能是一个副作用。

当然,当您需要有序映射时,没有编译器会阻止您使用无序映射,但这不太可能很好地工作,以至于读者可能会认为这不仅仅是一个错误。

【讨论】:

    【解决方案2】:

    以上所有内容的小补充:

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

    【讨论】:

      【解决方案3】:

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

      整数检验

      测试场景包括使用顺序和随机键以及长度在 [17:119] 范围内的字符串值(以 17 的倍数)填充映射。使用元素计数在 [10:100000000] 范围内执行的测试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[string] 映射的时间安排。密钥字符串由随机 uint64_t 值组成,值字符串与其他测试中使用的相同。

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

      评估平台

      操作系统:Linux - OpenSuse Tumbleweed

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

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

      内存:64Gb

      【讨论】:

        【解决方案4】:

        如果您使用 Visual Studio 2010 编译项目 - 忘记字符串的 unordered_map。 如果您使用更现代的 Studio,例如 2017 - 那么 unordered_map 比有序地图快得多。

        【讨论】:

          【解决方案5】:

          我对@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
          

          【讨论】:

          • 感谢您的测试。为了确保我们没有测量噪音,我将其更改为多次执行每个操作(并将计数器而不是 1 插入到地图中)。我在不同数量的键(从 2 到 1000)上运行它,并且映射中最多约 100 个键,std::map 通常优于 std::unordered_map,尤其是对于整数键,但 ~100 个键似乎失去了优势和@ 987654325@开始中奖。将已经排序的序列插入std::map 是非常糟糕的,你会得到最坏的情况(O(N))。
          【解决方案6】:

          不要忘记map 保持其元素有序。如果你不能放弃,显然你不能使用unordered_map

          另外要记住的是unordered_map 通常使用更多的内存。 map 只有几个管理指针和每个对象的内存。相反,unordered_map 有一个大数组(在某些实现中这些数组可能会变得很大),然后每个对象都有额外的内存。如果您需要内存感知,map 应该会更好,因为它缺少大数组。

          所以,如果您需要纯粹的查找-检索,我会说unordered_map 是要走的路。但总要有所取舍,买不起,就不能用。

          仅根据个人经验,我发现在主实体查找表中使用 unordered_map 而不是 map 时,性能(当然是测量的)有了巨大的改进。

          另一方面,我发现它在重复插入和删除元素时要慢得多。这对于相对静态的元素集合来说非常有用,但是如果您要进行大量的插入和删除,那么散列 + 分桶似乎会加起来。 (注意,这是经过多次迭代。)

          【讨论】:

          • 关于 unordered_map 与 map(或向量与列表)的 large(r) 内存块属性的另一件事,默认进程堆(此处为 Windows)是序列化的。在多线程应用程序中大量分配(小)块非常昂贵。
          • RA:如果您认为这对任何特定程序都很重要,您可以使用自己的分配器类型与任何容器相结合来控制它。
          • 如果您知道unordered_map 的大小并在开始时保留它 - 您仍然会为多次插入付出代价吗?比如说,您只在构建查找表时插入一次 - 然后只从它读取。
          • @thomthom 据我所知,在性能方面不应该有任何损失。性能受到影响的原因是,如果数组变得太大,它将对所有元素进行重新散列。如果您调用reserve,它可能会重新散列现有元素,但如果您在开始时调用它,那么应该不会受到惩罚,至少根据cplusplus.com/reference/unordered_map/unordered_map/reserve
          • 我很确定在记忆方面它是相反的。假设无序容器的默认加载因子为 1.0:桶的每个元素有一个指针,桶中的下一个元素每个元素有一个指针,因此最终每个元素都有两个指针和数据。另一方面,对于有序容器,典型的 RB-tree 实现将具有:三个指针(左/右/父)加上一个颜色位,由于对齐而需要第四个单词。即每个元素有四个指针加上数据。
          【解决方案7】:

          如果您想比较您的 std::mapstd::unordered_map 实现的速度,您可以使用 Google 的 sparsehash 项目,它有一个 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)
          

          【讨论】:

          • 看起来无序地图在大多数操作中都胜过地图。插入事件...
          • sparsehash 不再存在。它已被删除或撤下。
          • @User9102d82 我已编辑问题以引用waybackmachine link
          • 只是为了确保其他人也注意到除时间之外的其他数字:这些测试是使用 4 字节对象/数据结构(也称为 int)完成的。如果你存储的东西需要更重的散列或更大(使复制操作更重),标准映射可能很快就会有优势!
          【解决方案8】:

          此处并未真正充分提及的重大差异:

          • map 使所有元素的迭代器保持稳定,在 C++17 中,您甚至可以将元素从一个 map 移动到另一个,而不会使它们的迭代器失效(并且如果在没有任何潜在分配的情况下正确实施)。
          • map 单个操作的时间通常更加一致,因为它们从不需要大量分配。
          • unordered_map 使用在 libstdc++ 中实现的 std::hash 如果输入不受信任的输入,则容易受到 DoS 攻击(它使用带有恒定种子的 MurmurHash2 - 并不是说​​种子真的有帮助,请参阅 https://emboss.github.io/blog/2012/12/14/breaking-murmur-hash-flooding-dos-reloaded/)。
          • 有序可实现高效的范围搜索,例如遍历 key ≥ 42 的所有元素。

          【讨论】:

            【解决方案9】:

            总结

            假设顺序不重要:

            • 如果您要构建一次大表并进行大量查询,请使用std::unordered_map
            • 如果您要构建小表(可能少于 100 个元素)并进行大量查询,请使用std::map。这是因为上面的读取是O(log n)
            • 如果您要经常更换桌子,那么可能std::map 是不错的选择。
            • 如果您有疑问,请使用std::unordered_map

            历史背景

            在大多数语言中,无序映射(也称为基于哈希的字典)是默认映射,但在 C++ 中,您将有序映射作为默认映射。那是怎么发生的?有些人错误地认为 C++ 委员会以其独特的智慧做出了这个决定,但不幸的是,事实比这更丑陋。

            believed 广泛认为 C++ 最终以有序映射为默认值,因为没有太多关于如何实现它们的参数。另一方面,基于散列的实现有很多事情要谈。因此,为了避免标准化的僵局,他们just got along 使用有序地图。 2005 年左右,许多语言已经有了很好的基于散列的实现,因此委员会更容易接受新的std::unordered_map。在一个完美的世界中,std::map 将是无序的,我们会将 std::ordered_map 作为单独的类型。

            性能

            下面两张图应该不言自明(source):

            【讨论】:

            • 有趣的数据;您在测试中包含了多少个平台?
            • 根据您在此处发布的 2 张图片,由于 std::unordered_map 的性能始终优于 std::map,为什么在进行大量查询时我应该将 std::map 用于小表?
            • 图表显示了 0.13M 或更多元素的性能。如果你有小(可能是
            【解决方案10】:

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

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

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

            【讨论】:

            • 哈希调整大小可以通过dynamic hashing 技术来抑制,该技术包括有一个过渡期,每次插入一个项目时,你也会重新散列k 其他项目。当然,这意味着在过渡期间您必须搜索 2 个不同的表...
            • "使用长字符串作为键,std::map 可能会在 unordered_map 开始搜索之前完成搜索。" -- 如果集合中不存在密钥。如果存在,那么当然需要比较全长以确认匹配。但同样unordered_map 需要通过完整比较来确认哈希匹配,因此这完全取决于您要对比的查找过程的哪些部分。
            • 您通常可以根据数据的知识替换散列函数。例如,如果您的长字符串在最后 20 个字节中的变化大于前 100 个字节,则只需对最后 20 个字节进行哈希处理。
            【解决方案11】:

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

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

            这在实践中如何发挥作用是哈希表每隔一段时间就会“打嗝”一次 O(n) 操作,这可能是您的应用程序可以容忍的,也可能不是。如果它不能容忍它,你会更喜欢 std::map 而不是 std::unordered_map。

            【讨论】:

              【解决方案12】:

              发件人:http://www.cplusplus.com/reference/map/map/

              “在内部,地图中的元素始终按照其内部比较对象(比较类型)指示的特定严格弱排序标准按其键排序。

              map 容器在通过键访问单个元素时通常比 unordered_map 容器慢,但它们允许根据顺序对子集进行直接迭代。”

              【讨论】:

                【解决方案13】:

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

                对于map 实现,完成工作需要 200 毫秒。对于unordered_map + mapunordered_map 插入需要 70 毫秒,map 插入需要 80 毫秒。所以混合实现要快 50 毫秒。

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

                【讨论】:

                  【解决方案14】:

                  我只想指出...unordered_maps 有很多种。

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

                  这就是在 STL 中添加 unordered_map 时最让我担心的问题:他们将不得不选择一个特定的实现,因为我怀疑他们会走 Policy 的道路,所以我们将被困在平均使用的实现,其他情况则没有...

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

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

                  所以目前我更喜欢std::map 或者loki::AssocVector(用于冻结数据集)。

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

                  【讨论】:

                  • +1:有效点——当我使用自己的实现时,生活变得更轻松了——至少我知道 在哪里它糟透了:>
                  【解决方案15】:

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

                  什么是好的散列函数取决于数据的特征;因此,如果我不打算查看自定义哈希函数(但以后肯定会改变主意,而且很容易,因为我在所有东西附近都键入了该死的),即使选择默认值以对许多数据源执行得体,我发现有序map 的性质最初足以提供帮助,在这种情况下,我仍然默认使用 map 而不是哈希表。

                  此外,您甚至不必考虑为其他(通常是 UDT)类型编写散列函数,只需编写 op

                  【讨论】:

                  • @Roger,你知道 unordered_map 最好映射的元素的大致数量吗?无论如何,我可能会为它写一个测试......(+1)
                  • @Kornel:不需要太多;我的测试使用了大约 10,000 个元素。如果我们想要一个真正准确的图表,您可以查看mapunordered_map 之一的实现,具有特定平台和特定缓存大小,并进行复杂分析。 :P
                  • 取决于实现细节、编译时调整参数(如果您正在编写自己的实现,则易于支持),甚至是用于测试的特定机器。就像其他容器一样,委员会只设定广泛的要求。
                  猜你喜欢
                  • 2013-09-13
                  • 2011-09-23
                  • 2013-02-05
                  • 1970-01-01
                  • 2013-11-25
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多