地图会越长越慢吗?我不是在讨论迭代它,而是像.find()
.insert()
和.at()
这样的操作。
例如,如果我们map<int, Object> mapA
包含100'000'000个元素,而map<int, Object> mapB
只包含100个元素。
执行mapA.find(x)
和mapB.find(x)
会不会有任何性能差异?
答案 0 :(得分:9)
std::map
上的查找和插入操作的复杂性在于地图中元素的数量对数。因此,随着地图变大,它变得越来越慢,但只有它慢慢地慢慢地非常(比元素数中的任何多项式慢)。要实现具有此类属性的容器,操作通常采用二进制搜索的形式。
想象它的速度有多慢,每次加倍元素数时,基本上需要进一步操作。因此,如果您需要在具有4000个元素的地图上执行 k 操作,则需要在具有8000个元素的地图上执行 k + 1操作, k + 2对于16000个元素,等等。
相比之下,std::unordered_map
并没有为您提供元素的排序,作为回报,它为您提供了平均的复杂性。此容器通常实现为哈希表。 &#34;平均而言#34;意味着查找一个特定元素可能需要很长时间,但查找许多随机选择的元素所需的时间除以查找元素的数量,并不取决于容器大小。无序地图为您提供的功能更少,因此可以为您提供更好的性能。
但是,在选择要使用的地图时要小心(提供排序并不重要),因为渐近成本并不能告诉您有关实际挂钟成本的任何信息。无序地图操作中涉及的散列成本可能会产生一个显着的常数因子,只会使无序地图比大尺寸的有序地图更快。此外,无序地图缺乏可预测性(以及使用所选键的潜在复杂性攻击)可能使得有序地图在您需要控制最坏情况而不是平均值的情况下更可取。
答案 1 :(得分:2)
C ++标准只要求std::map
具有对数查找时间;并不是它是任何特定基数的对数或任何特定的恒定开销。
因此,要求“1亿张地图比100张地图慢多少倍”是荒谬的;它很可能很容易占据开销,因此操作速度大致相同。对于小尺寸,时间增长甚至可能是指数级的!按照设计,这些东西都不能完全从规范中推导出来。
此外,您询问时间,而不是操作。这很大程度上取决于访问模式。 To use some diagrams from Paul Khong's (amazing) blog on Binary searches,重复搜索的运行时间(查看stl
,绿松石线)几乎完全是对数的,
但是一旦你开始进行随机访问,由于在1级缓存之外的内存访问,性能变得非常非对数:
请注意,goog
是指Google的dense_hash_map
,类似于unordered_map
。在这种情况下,即使它不能避免在较大尺寸下性能下降。
对于大多数情况,后一个图表可能更有说服力,并且表明查找大小为100 map
的随机索引的速度成本将比尺寸为500'000的地图低大约10倍。 dense_hash_map
会降低更糟,因为它会从几乎免费变为肯定不是免费的,尽管总是多比STL {更快< {1}}。
一般来说,在提出这些问题时,理论上的方法只能给你非常粗略的答案。快速查看实际基准和常数因素的考虑可能会显着调整这些粗略答案。
现在,还要记住你在谈论map
,这与map<int, Object>
非常不同;如果set<uint32_t>
很大,这将强调缓存未命中的成本,并且不再强调遍历的成本。
关于哈希映射的快速说明:它们的时间复杂度通常被描述为恒定时间,但这并非严格正确。对于查找而言,大多数哈希映射都会为您提供具有非常高可能性的常量时间,而对于插入,以非常高的可能性分摊。
前者意味着对于大多数哈希表,有一个输入使它们的执行不是最佳的,而对于用户输入,这可能是危险的。出于这个原因,Rust使用默认的加密哈希,Java的HashMap
解决了与二进制搜索和CPython randomizes hashes的冲突。通常,如果您将哈希表暴露给不受信任的输入,则应确保使用此类缓解措施。
对于你担心攻击者的情况,有些像Cuckoo哈希,比概率(在约束数据类型上,给定special kind of hash function)做得更好,incremental resizing消除了摊还的时间成本(假设便宜的分配),但两者都不常用,因为这些很少是需要解决的问题,解决方案也不是免费的。
那就是说,如果你正在努力想到为什么我们要经历使用无序地图的麻烦,请回顾一下图表。他们很快,你应该使用它们。