为什么std :: shuffle比std :: sort慢(甚至慢)?

时间:2015-09-15 13:02:16

标签: c++ performance sorting c++11 shuffle

考虑测量执行时间和执行交换次数的简单代码:

#include <iostream>

#include <vector>
#include <random>
#include <chrono>
#include <algorithm>

struct A {
    A(int i = 0) : i(i) {}
    int i;
    static int nSwaps;

    friend void swap(A& l, A& r)
    {
        ++nSwaps;
        std::swap(l.i, r.i);
    }

    bool operator<(const A& r) const
    {
        return i < r.i;
    }
};

int A::nSwaps = 0;

using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::milliseconds;


int main()
{
    std::vector<A> v(10000000);

    std::minstd_rand gen(std::random_device{}());
    std::generate(v.begin(), v.end(), [&gen]() {return gen();});

    auto s = high_resolution_clock::now();
    std::sort(v.begin(), v.end());
    std::cout << duration_cast<milliseconds>(high_resolution_clock::now() - s).count() 
        << "ms with " << A::nSwaps << " swaps\n";

    A::nSwaps = 0;
    s = high_resolution_clock::now();
    std::shuffle(v.begin(), v.end(), gen);
    std::cout << duration_cast<milliseconds>(high_resolution_clock::now() - s).count() 
        << "ms with " << A::nSwaps << " swaps\n";
}

程序的输出取决于编译器和机器,但它们的性质非常相似。在我使用VS2015的笔记本电脑上,我得到了1044毫秒,其中有1亿个交换用于排序,824毫秒用1000万个交换用于随机播放。

libstdc ++和libc ++进行排序(~50M)的交换次数是两倍,结果如下。 Rextester给了我类似的结果:gcc排序854ms,重排565ms,clang排序874ms,洗牌648ms。 ideone和coliru显示的结果更加激烈:ideone排序 1181ms ,洗牌 1292ms coliru排序 1157ms ,洗牌 1461ms

那么罪魁祸首是什么?为什么交换排序的5到10倍几乎与简单的shuffle一样快或甚至更快?我甚至没有考虑std::sort中的比较和更复杂的逻辑,包括选择插入,堆或快速排序算法等。我怀疑它是随机引擎 - 我甚至选择了最简单的{{1}这基本上是一个整数乘法和一个模数。是否缓存未命中使得shuffle相对较慢?

PS:简单std::minstd_rand

的行为相同

2 个答案:

答案 0 :(得分:4)

std::random_shuffle通常如下工作:

//random(k) generates uniform random from 0 to k-1 inclusive
for (int i = 1; i < n; i++)
  swap(arr[i], arr[random(i + 1)]);

所以我们可以在这里看到两个效率低下的原因:

  1. 随机数生成器通常很慢。
  2. 每个交换使用向量中的完全随机元素。当数据大小很大时,整个向量不适合CPU缓存,因此每次访问都必须等到从RAM读取数据。
  3. 说到第2点,像quicksort这样的排序算法对缓存更友好:大部分内存访问都是缓存。

答案 1 :(得分:1)

首先,std::sort不需要使用不合格的swap。它不是自定义点,您不能依赖于通过ADL找到的自己的用户定义swap。但即便如此,sort也可以使用std::rotateswap也可以memmove。这不会被您的实施计算在内。

其次,标准库仅指定渐近复杂度,O(N)std::shuffleO(N log N)std::sort。因此,您应该测量N的不同值(例如,从65K到65M量的元素的2的幂)并测量缩放行为。对于较小的Nsort的比例常数可能远小于shuffle的比例常数,因为它必须调用潜在的昂贵的随机生成器。

更新:确实看起来常数因素和/或缓存效应是罪魁祸首(正如@stgatilov所指出的那样)。在std::sort被调用后,请参阅this DEMO我在哪里运行std::shuffle数据。 sort的运行时间约为shuffle的一半,互换次数增加5倍。