【问题标题】:Why is std::rotate so fast?为什么 std::rotate 这么快?
【发布时间】:2014-02-05 07:21:10
【问题描述】:

为什么std::rotate 比 cplusplus.com 描述的等效函数快这么多?

cplusplus.com 的实现:

template <class ForwardIterator>
  void rotate (ForwardIterator first, ForwardIterator middle, ForwardIterator last)
{
  ForwardIterator next= middle;

  while (first != next)
  {
    swap (*first++, *next++);

    if(next == last)
        next= middle;
    else if (first==middle)
        middle= next;
  }
}

我有两种完全相同的插入排序算法,除了一种使用std::rotate,一种使用cplusplus.com的等效函数。我将它们设置为使用 1000 个 int 元素对 1000 个向量进行排序。使用std::rotate的排序耗时0.376秒,另一个耗时8.181秒。

这是为什么?我不打算尝试制作比 STL 函数更好的东西,但我仍然很好奇。

【问题讨论】:

  • 许多可能的原因:std::rotate 是使用特定于架构的程序集实现的,或者您可能只是没有使用优化标志进行编译
  • 这在很大程度上取决于您的标准库实现。你看过std::rotate的来源吗?
  • 你使用什么编译器,什么优化级别?
  • 看看你的编译器的实现——g++有很多不同的选择算法
  • 制作一个自定义的std::swap 实现并记录它被调用的次数。

标签: c++ algorithm sorting c++11 stl


【解决方案1】:

编辑:

由于没有给出上下文,不清楚你的代码是调用std::swap()还是其他swap(a,b)之类的算法

T tmp = a; a = b; b = tmp;

ab 是 1000 个 ints 的向量时,这将复制所有向量元素 3 次。 std::swap() 的专用版本用于像 std::vector&lt;T&gt; 这样的容器调用容器 a.swap(b) 方法,本质上只交换容器的动态数据指针。

此外,对于不同的迭代器类型,std::rotate() 实现可以利用一些优化(请参阅下面我较早的,可能具有误导性的答案)。


警告:std::rotate() 的实现依赖于实现。 对于不同的迭代器类别,可以使用不同的算法 (例如,在 GNU g++ 的 bits/stl_algo.h 标头中查找 __rotate()。

要将n 元素移动m=std::distance(first,middle) 一个简单(简单)的算法,比如m 旋转一个元素需要O(n*m) 移动或复制操作。但是只需要 O(n) 次移动,当每个元素被直接放置到它的正确位置时,这会导致(大约)m 倍的算法速度。

举例说明:将字符串s = "abcdefg" 旋转三个元素:

abcdefg : store 'a' in temporary place
dbcdefg : move s[3] to s[0] (where it belongs in the end, directly)
dbcgefg : move s[6] to s[3]
dbcgefc : move s[9%7] to s[6] (wrapping index modulo container size: 9%7 == 2)
dbfgefc : move s[5] to s[2]
dbfgebc : move s[1] to s[5] (another wrapping around)
defgebc : move s[4] to s[1]
defgabc : move 'a' from temporary place to s[4]

对于最大公约数为 1 的 nm,您现在完成了。否则,您必须为第一个m 连续元素重复该方案n/m 时间(此处假设为n &gt; m)。 这个稍微复杂一点的算法要快得多。

对于双向迭代器,可以使用另一个传说中的 O(3n) 算法,称为“翻转手”。根据 Jon Bentley 的书 Programming Pearls,它在早期的 UNIX 编辑器中用于移动文本:

将你的手放在你面前,一只在另一只上面,竖起大拇指。现在

  1. 转一只手。
  2. 转动另一个。
  3. 转动两者,相互连接。

在代码中:

reverse(first, middle);
reverse(middle, last);
reverse(first, last);

对于随机访问迭代器,大​​块内存可以通过swap_ranges()(或对于 POD 类型的memmove() 操作)进行重定位。

利用汇编操作的微优化可以提供少量的额外加速,它可以在 fasted 算法之上完成。

使用连续元素而不是在内存中“四处跳动”的算法也可以减少现代计算机架构上的缓存未命中次数。

【讨论】:

  • 谢谢。我想我需要大量阅读迭代器、算法和 STL 才能真正理解这里的所有因素。有机会我会研究替代算法 =)
  • 引用的算法 OP 执行 O(n) 次移动,而不是 O(nm)。
【解决方案2】:

正如评论者已经说过的,这取决于您的标准库实现。但是您发布的代码即使对于 前向迭代器 也是有效的。因此,它的要求非常低(只有这些迭代器可以递增和取消引用)。

Stepanov 的经典著作Elements of Programming 用一整章 (10) 来讲述rotate 和其他重排算法。对于前向迭代器,代码中的一系列交换给出O(3N) 分配。对于双向迭代器,对reverse 的三个连续调用会产生另一个O(3N) 算法。对于随机访问迭代器std::rotate 可以通过定义索引排列 w.r.t 实现为O(N) 分配。到起始迭代器first

上述所有算法都是就地的。使用内存缓冲区,随机访问版本可能会受益于更大的缓存局部性memcpy()memmove()(如果基础值类型是 POD),其中可以交换整个连续内存块。如果您的插入排序是在数组或std::vector 上完成的,那么您的标准库很可能会利用这种优化。

TL;DR:相信您的标准库,不要重新发明轮子!

【讨论】:

  • > 对于随机访问迭代器,std::rotate 很可能受益于 memmove() 优化,其中可以交换整个连续内存块。一般来说,这是不正确的。如果底层数据类型是 POD(plain old data)类型,那么可以使用 memmove/memcpy。否则,对象的复制/移动构造函数必须被调用。
  • 谢谢。这些cmets和答案给了我很多见识。看来我还有很多事情要做,才能完全理解发生了什么……
  • @bradshire 您想要学习和理解的事实是重要的部分。现在是永无止境的努力!
  • 在大 O 表示法中,O(3N) 与 O(N) 相同,因为我们只对函数行为感兴趣。
猜你喜欢
  • 2021-06-10
  • 2016-05-12
  • 1970-01-01
  • 2015-01-07
  • 1970-01-01
  • 2014-11-23
  • 2017-08-28
  • 2022-01-14
  • 2018-03-22
相关资源
最近更新 更多