【问题标题】:Why is inserting multiple elements into a std::set simultaneously faster?为什么将多个元素同时插入 std::set 更快?
【发布时间】:2011-12-31 11:53:58
【问题描述】:

我正在阅读:

“C++ 标准库:Nicolai M. 的教程和参考。 约苏蒂斯”

我在关于 Sets & Multisets 的部分。我遇到了关于插入和删除元素的一行:

“如果在使用多个 元素,您对所有元素使用单个调用而不是多个 来电。”

我远非数据结构大师,但我知道它们是用红黑树实现的。我不明白的是,STL 实现者如何编写一个算法来以更快的方式一次插入多个元素?

谁能解释一下为什么这句话对我来说是正确的?

【问题讨论】:

  • 不是说只是因为you use a single call for all elements rather than multiple calls.
  • @Shahbaz:一点也不,它说使用单个呼叫时速度更快,但没有给出任何理由。
  • @MatthieuM。啊对不起!我在这句话之前想象一个因为(这表明我很累,也许我今天应该停止编码)
  • 没关系,我知道那种感觉。我不得不删除昨晚的 2 个答案,因为在 14 小时工作日后,我试图在火车上通过手机进行 SO - 至少可以说这不是我最好的工作:)

标签: c++ data-structures stl


【解决方案1】:

有两个原因:

1) 对多个元素进行一次调用,而不是 N 次调用。

2) 插入操作检查每个插入的元素是否在容器中已经存在具有相同值的另一个元素。这可以在将多个元素插入在一起时进行优化。

【讨论】:

  • 1) 不一定更快,模板调用往往是内联的,因此如果插入仅循环通过元素,您将一无所获。 2)我认为问题的目标是了解优化。如果要插入的元素已排序,我会理解性能提升,但由于它们不是我承认我想知道可以应用什么样的优化。
  • 对于 1),在信任您最喜欢的 C++ 编译器之前尝试比较汇编输出。 :) 对于 2),我有一些优化的想法,但我不知道 STL 使用哪种优化。我只是想如果我知道如何优化它,STL 的创建者应该知道的更好,对吧?
  • 也许您可以在答案中包含一些您想到的优化?我认为,就像@MatthieuM. 一样,这比仅仅说可以进行优化更具建设性。
  • @EmirAkaydın:我不信任我的编译器,当我遇到性能问题来识别热点时,我只是将 Callgrind 扔给问题 :) 并且通常只是避免过早的悲观化,这在大多数情况下就足够了次。
  • 但是,如果我不插入重复项,您所描述的内容也可能是一种悲观。
【解决方案2】:

我的第一个想法是,它可能仅在插入/擦除整个范围后才重新平衡树。由于整个操作在实践中是内联的,这似乎比函数调用的次数更有可能。

检查本地计算机上的 GCC 标头,情况似乎并非如此 - 无论如何,我不知道如何在减少重新平衡活动和可能增加中间插入到不平衡树的搜索时间之间进行权衡,会解决的。

也许它被认为是一个 QoI 问题,但无论如何,使用 最具表现力 方法可能是最好的,不仅仅是因为它可以节省您编写 for 循环并最清楚地表明您的意图,但是因为它让库编写者可以在未来进行更积极的优化,而无需了解和更改代码。

【讨论】:

  • 我确实想到你可能只需要一次重新平衡就可以进行批量插入。我不确定您是否可以在批量插入后正确地重新平衡一棵树,而无需做同样多的工作。您认为事后重新平衡会比在每次插入时重新平衡更快吗?
  • 在某些情况下,您可以将它们插入到正确的位置而不做 any 平衡(例如插入空集,或者如果所有新节点都是同一级别的叶子),但是对于每个单独的插入,这些都需要关闭平衡。 (除非你比我方式更狡猾)
【解决方案3】:

内存管理可能是一个很好的理由。在这种情况下,它可以只分配一次内存。如果单独调用所有元素,则所有调用都尝试单独分配内存。据我所知,大多数setmap 实现都试图将内存保持在同一页面中,或者页面靠近在一起以最小化页面错误。

【讨论】:

  • 它不能将它们分配为一个数组(为了速度),因为它可能需要在不同的时间释放它们。如果没有自定义分配器,set/map 将如何尝试将内存保持在同一页面上?
【解决方案4】:

对此我不确定,但我认为如果插入的元素数量小于集合中的元素数量,那么在执行插入之前对插入的范围进行排序会更有效。这样,所有值都可以在树上一次插入,并且可以轻松消除插入范围内的重复项(或者在多集的情况下非常快速地插入)。

当然,这种优化只有在输入迭代器允许对输入范围进行排序的情况下才有可能(即,如果它们是随机迭代器)。

【讨论】:

  • 标准规定[i,j) 中的每个元素最多被取消引用一次,因此排序是不可能的。
  • @MooingDuck:哦,对了,我忘了InputIterators 只允许单通道算法。
  • 但是,您确定此限制适用于所有专业吗?我的意思是,如果一种算法专门针对给定的迭代器概念(比如RandomIterator),为什么这种专门化会受到对其他迭代器概念的要求的限制?例如,std::distancestd::advance 都适用于 InputIterators,但它们专用于 RandomIterator 并适用于 InputIterators 不支持的 operator-operator+
  • 标准说的是关于容器,而不是迭代器。一个符合标准的容器不能多次取消引用任何元素。我想它可以制作一个副本,然后对其进行排序......
  • @MooingDuck:我不明白你的意思:首先,“一个容器不能多次取消引用任何元素”是什么意思?您是指容器的成员函数(及其构造函数)吗?您能否指出您所引用的段落,以便我更好地理解?其次,如果这是标准所说的,我不明白这个决定背后的理由是什么。为什么要阻止实现为某些特定输入提供某些成员函数的优化实现?
【解决方案5】:

您所引用的内容是错误的。插入到 std::set 是 O(log n),除非您将 insert() 重载与位置迭代器一起使用,在这种情况下,当位置有效时,它是 摊销 O(n)。 但是,如果您将范围重载与 已排序 元素一起使用那么您将获得 O(n) 插入。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-07-26
    • 1970-01-01
    • 2011-01-25
    • 1970-01-01
    • 2018-02-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多