【问题标题】:Unordered map vs vector无序地图与矢量
【发布时间】:2016-01-29 16:43:39
【问题描述】:

我正在构建一个小型 2d 游戏引擎。现在我需要存储游戏对象的原型(所有类型的信息)。我猜一个容器最多有几千个元素,所有元素都具有唯一键,并且在第一次加载后不会删除或添加任何元素。键值是一个字符串。

各种线程将运行,我需要向每个人发送一个密钥(或索引),并通过该访问权限访问仅对这些线程可用的其他信息(如渲染进程的纹理或混音器进程的声音)。

通常我使用向量,因为它们访问已知元素的速度更快。但是我看到,如果我使用 ::at 元素访问,无序映射通常也具有恒定的速度。这将使代码更简洁,也更易于维护,因为我将处理更易于理解的人造字符串。

所以问题是,访问vector[n]unorderedmap.at("string") 之间的速度差异与他的收益相比可以忽略不计?

据我了解,在程序的不同部分访问各种地图,不同的线程只用一个“名称”运行对我来说很重要,而且速度差异不是很大。但我太缺乏经验,无法确定这一点。虽然我发现了有关它的信息,但我似乎无法真正理解我是对还是错。

感谢您的宝贵时间。

【问题讨论】:

  • 唯一真正了解的方法是尝试每一个并测量。
  • Trie 是另一种选择,如果您将字符串保留在键上。
  • 请务必忽略有关缓存的错误答案。

标签: c++ c++11 vector unordered-map


【解决方案1】:

作为替代方案,您可以考虑使用有序向量,因为向量本身不会被修改。您可以使用 STL lower_bound 等轻松地自己编写实现,或者使用库中的实现 ( boost::flat_map)。

在这种情况下,有一个关于容器性能的blog post from Scott Meyers。他做了一些基准测试,得出的结论是unordered_map 可能是一个非常好的选择,它很有可能是最快的选择。如果你有一组受限的键,你还可以计算一个最小的最优散列函数,例如with gperf

但是,对于这类问题,第一条规则是衡量自己。

【讨论】:

  • 为 Scott Meyers 的博文+1。这基本上解决了围绕一个类似问题长达一个小时的讨论。每个人都应该阅读它:)
【解决方案2】:

我的问题是通过给定的 std::string 类型在容器上查找记录作为密钥访问。考虑到只有 EXISTS 的键(找不到它们不是一个选项),并且这个容器的元素只在程序开始时生成,之后就再也没有接触过。

我非常担心无序地图不够快。所以我测试了它,我想分享结果,希望我没有弄错一切。 我只是希望这可以帮助像我这样的其他人并获得一些反馈,因为最终我是初学者。 所以,给定一个这样随机填充的记录结构:

struct The_Mess 
{   
    std::string A_string;
    long double A_ldouble;
    char C[10]; 
    int* intPointer;
    std::vector<unsigned int> A_vector;
    std::string Another_String;
}        

我做了一个无序的映射,让 A_string 包含记录的键:

std::unordered_map<std::string, The_Mess> The_UnOrdMap;

和我按 A_string 值排序的向量(包含键):

std::vector<The_Mess> The_Vector;

还有一个索引向量已排序,并用于作为第三种方式访问​​:

std::vector<std::string> index;

密钥将是长度为 0-20 个字符的随机字符串(我想要最坏的情况),其中包含大写字母和普通字母以及数字或空格。

因此,简而言之,我们的竞争者是:

  1. 无序映射我测量程序执行的时间:

    record = The_UnOrdMap.at( key ); 记录只是一个 The_Mess 结构。

  2. Sorted Vector 测量语句:

    low = std::lower_bound (The_Vector.begin(), The_Vector.end(), key, compare); record = *low;

  3. 排序索引向量:

    low2 = std::lower_bound( index.begin(), index.end(), key); indice = low2 - index.begin(); record = The_Vector[indice];

时间以纳秒为单位,是 200 次迭代的算术平均值。我有一个向量,我在每次迭代时都对它进行洗牌,其中包含所有键,并且在每次迭代时,我循环遍历它并以三种方式查找我在这里拥有的键。 所以这是我的结果:

我认为首字母尖峰是我的测试逻辑的错误(我迭代的表仅包含到目前为止生成的键,因此它只有 1-n 个元素)。因此,第一次进行 200 次 1 键搜索迭代。 2 个键的 200 次迭代第二次搜索等...

无论如何,最终最好的选择似乎是无序映射,考虑到它的代码少得多,它更容易实现,并且将使整个程序更易于阅读,并且可能更易于维护/修改。

【讨论】:

    【解决方案3】:

    您还必须考虑缓存。在std::vector 的情况下,您在访问元素时将获得非常好的缓存性能 - 当访问 RAM 中的一个元素时,CPU 将 缓存 附近的内存值,这将包括您的附近的部分 std::vector .

    当您使用std::map(或std::unordered_map)时,这不再适用。映射通常实现为self balancing binary-search trees,在这种情况下,值可以分散在 RAM 中。这对缓存性能造成了很大的命中,尤其是当地图变得越来越大时,因为 CPU 无法缓存您将要访问的内存。

    您必须运行一些测试并测量性能,但缓存未命中会极大地损害您的程序的性能。

    【讨论】:

    • 介意自我介绍吗?
    • 关于 OP 的问题,您可能希望链接到散列地图描述而不是地图/树描述。尽管如此,它可能对缓存同样不利,所以您的建议仍然适用。
    • @Daniel 没错,我没有给出明确的建议或答案。但是在选择数据结构时必须考虑缓存性能。这就是我回答的重点:)
    • 我想我得到了答案,考虑到:我不能强制 c++ 缓存东西,而且这个地图/向量将被大量使用,大约每 1/60 的千次第二。所以我想我会坚持使用向量,也许当我完成后我会运行一些测试。谢谢!
    • @Džanan unordered_map 不是一棵树。
    【解决方案4】:

    您最有可能获得相同的性能(差异无法衡量)。

    与某些人似乎相信的相反,unordered_map 不是二叉树。底层数据结构是一个向量。因此,缓存位置在这里无关紧要 - 它与向量相同。当然,如果由于散列函数不好而发生碰撞,您将遭受痛苦。但是,如果您的密钥是一个简单的整数,则不会发生这种情况。因此,访问哈希映射中的元素将与访问向量中的元素完全相同,花费时间获取整数的哈希值,这实际上是不可测量的。

    【讨论】:

    • OP 现在收到了 2 个相互矛盾的答案。进一步解释您的答案不会有帮助吗?如果缓存位置在这里无关紧要,我会非常有兴趣了解原因。
    • @DanielStrul,因为 unordered_map 不是树。
    • 但是 unordered_map 确实有为每次访问重新计算哈希的性能成本。因此,性能劣势主要取决于拥有良好的哈希算法——快速且冲突最少。
    • @Steger,从整数中获取哈希是微不足道的。
    猜你喜欢
    • 2010-09-08
    • 1970-01-01
    • 2014-08-23
    • 1970-01-01
    • 2021-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-07
    相关资源
    最近更新 更多