【问题标题】:vector vs map performance confusion矢量与地图性能混淆
【发布时间】:2014-08-23 22:36:21
【问题描述】:

编辑:我特别将std::vector 的线性 搜索操作与std::map 二进制 搜索操作进行比较,因为这似乎与Herb 的声明有关。我知道使用二分搜索会将性能从 O(N) 提高到 O(log N) 但这不会测试 Herb 的主张

Bjarne Stroustrup 和 Herb Sutter 最近都谈到了std::vector 在人们期望使用std::list 的情况下有多棒,因为在链表遍历期间缓存未命中的成本。 (见 http://channel9.msdn.com/Events/Build/2014/2-661 在 48 分钟标记处)

Herb 进一步声明,对有序向量的操作甚至比 std::map 还要快(参见 http://i.imgur.com/zX07TZR.png,取自上述 channel9 视频的 51:30 标记),我发现这很难理解。所以我创建了一个小测试来证明这一点,但很难重现这些结果:https://ideone.com/MN7DYK

这是测试代码:

template <typename C>
void test(std::string name, std::vector<int> shuffledInputValues, C & c)
{
   // fill container 'c' with values from 'shuffledInputValues' then erase them all
   {
      std::cout << "testing " << name << "..." << std::endl;
      timer t;

      for (auto val : shuffledInputValues) insert(c, val);
      for (auto val : shuffledInputValues) remove(c, val);
  }
}

// output:
// testing vector...99.189ms
// testing deque...120.848ms
// testing set...4.077ms

注意 std::vector 的执行速度比 std::set 慢一个数量级。当然这是我预期的结果,但我对 Herb 试图提出的说法感到困惑。

我做错了什么?还是我误解了 Herb 的说法?

关于我的测试应用的注意事项:

  • 它必须使用线性运算 - 练习的重点是演示 CPU 缓存魔法,这是 Herb 和 Bjarne 对练习的约束
  • 我没有为向量迭代尝试任何棘手的循环解开,但我相信迭代不会对性能产生太大影响
  • 我将循环限制为 10K 项,因为 ideone 在较大的集合上超时,但增加大小不会改变结果

编辑:请参阅https://ideone.com/916fVd 以获取仅比较查找性能的修改示例。线性搜索表现出相同的性能。

【问题讨论】:

  • @WhozCraig 是的,我同意。但他确实特别提到了线性查找操作......他顺便提到在现实世界中你会在排序的向量上使用二进制搜索,但他的主张是关于通过向量的线性迭代的性能(除非有人可以另外指出? 就像我说的那样,我很可能是错的!)
  • @Cechner:这很有趣,我很感兴趣,除非晚上发生任何事情,否则我会进行一个简单的测试!实际上,出于好奇,我不久前针对std::map 对boost::container::flat_map 进行了基准测试,并且确实发现flat_map 更快,当然使用的内存更少。由于每次缓存未命中都会受到持续的惩罚,但是我会说,当向量使用线性搜索时,对于大型集合 std::map 将始终击败 std::vector,这可能就是您正在经历的。
  • 我在大约 5 年前对此进行测量的经验是,在大约 200 个条目的编译器符号表的应用程序中,树与数组相得益彰(树节点的块分配以减少 malloc 开销)。我在用 C 编程,树的实现是我的。这些是约 16 个字节的小记录加上标识符字符串,这也是比较键。符号表操作中散布着大量应该挑战缓存的代码和数据活动。
  • 呵呵,这种话里总有夸张的成分,总想把重点说清楚。但是,我确实认为不应该使用诸如std::list 之类的链表,在实践中,我发现绝对零情况下它的特性更可取。如果您关心订单并且不经常更新收藏,请前往boost::container::flat_map/flat_set。如果您不关心订单,请选择 std::vector 并在最后一个元素上使用交换技巧。
  • @Cechner:是的,这就是为什么boost::container::flat_map 之类的东西并不总是替代品。根据我的经验,在insert 和erase 存在的情况下,您很少依赖迭代器的有效性。通常你会填满你的集合,并且在构建之后它保持相当稳定,在这种情况下,像存储这样的向量通常是一个巨大的净赢。如果迭代器有效性很重要,std::map/set 可能是最合适的。

标签: c++ stdvector


【解决方案1】:

在没有源代码以及有关编译器选项、硬件等信息的情况下,当然很难给出准确的答案。

几个可能的区别:

  • 我认为演讲者每次都在谈论向量的中间中的插入/删除(而在您的示例中,您总是添加到末尾并从开头删除);
  • 演讲者没有提及如何确定添加/删除哪些项目,但在这种情况下,我们不妨假设做出最小努力来确定这一点:您每次都进行向量访问,而插入值可以很简单地计算(例如,使用低成本的 PNRG 来确定要插入的下一个值)或始终相同,并且对于删除,每次都删除中间元素,因此不需要查找/计算值。李>

但是,正如另一位评论员所提到的,我会拿走一般原则而不是具体的数字/时间。从本质上讲,要传达的信息是:为了评估算法性能/可扩展性,您以为自己了解的“计数操作”在现代系统中不再适用。

【讨论】:

  • 感谢您的评论,但我的示例从随机位置插入/删除随机值...输入向量在执行定时操作之前被打乱
  • 啊,好吧,对不起,我的错——我应该仔细看看。尽管如此,您是否尝试过每次从中间插入/移除的时间? (虽然平均而言,您会期望相同数量的“操作”,但我们再次测试的效果是操作计数不一定按预期工作......)
【解决方案2】:

我找到了slides 以便于参考(我看不到图表,但我猜这可能是因为专有文件格式)。相关的幻灯片是第 39 号,它描述了正在解决的问题:

§ 生成 N 个随机整数并将它们插入到一个序列中,以便将每个整数按数字顺序插入到适当的位置。

§ 通过在序列中选择一个随机位置并删除那里的元素,一次删除一个元素。

现在,很明显链表不是解决这个问题的好选择。尽管列表在开头或中间插入/删除比向量好得多,但由于需要线性搜索,因此在 随机 位置插入/删除并不好。由于缓存效率更高,使用向量进行线性搜索要快得多。

Sutter 建议地图(或一般的树)似乎是该算法的自然选择,因为您得到 O(log n) 搜索。事实上,对于插入部分中较大的 N 值,它确实很容易击败向量。

但是来了。您需要删除第 n 个元素(对于随机 n)。这就是我认为您的代码作弊的地方。您按照插入的顺序删除元素,有效地将输入向量用作查找表以查找“随机”位置的元素值,以便您可以在 O(log n) 中搜索它。所以你实际上是在使用集合和向量的组合来解决问题。

常规二叉搜索树,例如用于std::map 或std::set(我假设 Sutter 使用)的二叉搜索树没有快速算法来查找第 n 个元素。 Here's one 据称平均为 O(log n),在最坏的情况下为 O(n)。但是std::map 和std::set 不提供对底层树结构的访问,所以对于那些你被困在有序遍历(如果我错了,请纠正我)的人来说,这又是一个线性搜索!实际上,我很惊讶地图版本与 Sutter 结果中的矢量版本具有竞争力。

对于 log(n) 复杂性,您需要一个结构,例如 Order statistic tree,遗憾的是标准库没有提供这种结构。有 GNU Policy-Based STL MAP,如 here 所示。

这是我为矢量 vs 集合 vs ost 制作的快速测试代码(vs 带有二进制搜索的矢量以获得良好的度量)https://ideone.com/DoqL4H Set 慢得多,而其他基于树的结构比向量快,这与 Sutter 的结果不符。

order statistics tree: 15.958ms
vector binary search: 99.661ms
vector linear search: 282.032ms
set: 2004.04ms

(N = 20000,差异只会更大,有利于具有更大值的ost)

简而言之,我得出了相同的结论,即 Sutter 的原始结果看起来很奇怪,但原因略有不同。在我看来,这次更好的渐近复杂度赢得了更低的常数因子。

请注意,问题描述不排除重复随机值的可能性,因此使用 map / set 而不是 multimap / multiset 有点欺骗 map / set,但我认为只有很小的意义当值域远大于N 时。此外,预先保留向量不会显着提高性能(当 N = 20000 时大约为 1%)。

【讨论】:

  • 哦,我写了一个订单统计树,虽然我以前从未听说过这个名字。
  • 这是正确答案 - 我没有意识到奇怪的(而且不切实际!)“删除”条款(谁删除了随机元素???)。我认为 Herb 将此测试包含在有关缓存位置的演示文稿中有点不诚实,因为“删除”子句只是表明 std::map 没有随机访问迭代器。 Bjarne 的原始幻灯片以这种方式更笼统,更不具有欺骗性。以前从未听说过 OST - 谢谢!
  • 对了,reddit 用户 nullsucks 也得出了同样的结论:reddit.com/r/cpp/comments/27hoi4/…
  • @Cechner Heh,我也不知道 ost。我认为必须有一个树结构可以很好地解决这个问题,并且在研究我的答案时遇到了它,所以感谢您提出这个问题!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-08
  • 1970-01-01
相关资源
最近更新 更多