qsort vs std :: sort的性能?


75

斯科特·迈耶斯(Scott Meyers)认为,在他的有效STL书中-项目46。他声称由于内联的事实,这种std::sort方法的速度要快670%std::qsort。我测试了自己,发现qsort更快:(!!有人可以帮我解释一下这种奇怪的行为吗?

#include <iostream>
#include <vector>
#include <algorithm>

#include <cstdlib>
#include <ctime>
#include <cstdio>

const size_t LARGE_SIZE = 100000;

struct rnd {
    int operator()() {
        return rand() % LARGE_SIZE;
    }
};

int comp( const void* a, const void* b ) {
    return ( *( int* )a - *( int* )b );
}

int main() {
    int ary[LARGE_SIZE];
    int ary_copy[LARGE_SIZE];
    // generate random data
    std::generate( ary, ary + LARGE_SIZE, rnd() );
    std::copy( ary, ary + LARGE_SIZE, ary_copy );
    // get time
    std::time_t start = std::clock();
    // perform quick sort C using function pointer
    std::qsort( ary, LARGE_SIZE, sizeof( int ), comp );
    std::cout << "C quick-sort time elapsed: " << static_cast<double>( clock() - start ) / CLOCKS_PER_SEC << "\n";
    // get time again
    start = std::clock();
    // perform quick sort C++ using function object
    std::sort( ary_copy, ary_copy + LARGE_SIZE );
    std::cout << "C++ quick-sort time elapsed: " << static_cast<double>( clock() - start ) / CLOCKS_PER_SEC << "\n";
}

这是我的结果:

C quick-sort time elapsed: 0.061
C++ quick-sort time elapsed: 0.086
Press any key to continue . . .

更新资料

有效的STL第三版(2001)
第7章使用STL编程
项目46:将函数对象而不是函数视为算法参数。

最好的祝福,


16
您让编译器进行优化了吗?调试/未优化的构建不会充分利用内联之类的优势。
爱德华·斯特朗奇

5
简而言之,了解快速排序的工作原理将带给您更好的测试方法:1.使用更大的数组,例如:大小为10 ^ 6,然后以降序填充数组999999 ... 4,3 ,2,1-这将导致排序变为O(n ^ 2),这样做将有效地说明为何内联比较器在此特定算法中产生如此大的差异。

11
@ Zenikoder-几乎没有实现,qsortsort将使用在反向排序输入上中断的快速排序实现。最常见的STLsort实现使用introsort,它对quicksort例程进行内省以确保其降级不会比O(n lg n)差,而且我相当有信心Cqsort例程使用类似的东西(或者至少是启发式的中位数) -3分)以防止这种情况。
templatetypedef

3
@Noah:根据关于artima SM的06文章:“我将首先从你们中许多人会发现的不可告人的表白开始:我20多年没有写过生产软件,而且我从未用C ++编写过生产软件。 ” 他称自己为C ++语言的考古学家/人类学家。

3
@Chan:宾利和麦克罗伊纸可以在这里找到:cs.ubc.ca/local/reading/proceedings/spe91-95/spe/vol23/issue11/...

Answers:


98

std :: clock()不是可行的计时时钟。您应该使用特定于平台的高分辨率计时器,例如Windows High Performance Timer。更重要的是,调用clock()的方式首先是将文本输出到控制台,该文本包含在时间中。这肯定会使测试无效。此外,请确保您使用所有优化进行了编译。

最后,我复制并粘贴了您的代码,qsort的值为0.016,std :: sort的值为0.008。


1
@DeadMG:谢谢!我更改为发布模式,并且得到了相似的结果。我真的很喜欢Scott Meyers,并且相信他的话;)

在这两种情况下似乎都输出了文本,因此它不能完全使结果无效。
OO Tiib

1
@Oo Tiib:输出的文本并不意味着不会同时输出。如果缓冲区大于第一个缓冲区但小于第二个缓冲区怎么办?现在,它必须在第二次调用之前刷新-但在第一次调用中没有刷新。噢亲爱的。我不是很高兴,因为我解决了所有上述问题,并且qsort现在快很多了。:(
小狗

@DeadMG:qsort快了多少?你能解释一下吗?

1
@DeadMG:std::qsort要求“此函数的返回值应分别通过返回负值,零或正值来表示elem1是小于,等于还是大于elem2。” operator<不满足该要求(特别是它仅返回0或1)。检查以确保std::sortstd::qsort生产在您的测试相同的结果:)(只是改变-<的结果qsort返回给我错误的答案)
比利·奥尼尔

20

我很惊讶没有人提到缓存。

在您的代码中,首先触摸ary和* ary_copy *,以便它们在qsort时驻留在缓存中。在qsort期间,* ary_copy *可能会被驱逐。在std :: sort时,必须从内存或较大(读较慢)的缓存级别中获取元素。当然,这将取决于您的缓存大小。

尝试逆转测试,即从运行std :: sort开始

正如某些人指出的那样;使数组更大将使测试更加公平。原因是大型阵列不太可能适合缓存。


5
令我惊讶的是,没有人提到任何衡量代码实际有效性的策略。您可以编写一个微型程序,对数百个元素进行排序,将所有内容加载到L1高速缓存中,并在记录的时间内完成读取,但这绝不能反映您的实际程序在具有数百个其他系统的系统上运行活动进程,进行上下文切换,因为您受计算限制,而调度程序讨厌您,同时对新泽西大小的数据集进行排序。使基准测试看起来像真实的应用程序。
Wexxor

13

两种未启用优化的排序算法应具有可比的性能。C ++sort容易被击败的原因qsort是编译器可以内联进行的比较,因为编译器具有有关用于执行比较的函数的类型信息。您是否在启用优化的情况下运行了这些测试?如果不是,请尝试将其打开并再次运行此测试。


谢谢!我正在使用Visual Studio,但我真的不知道如何打开优化。

3
@Chan:切换到使用“发布”版本。另外,还要确保不要在Visual Studio中为基准测试运行该程序-诸如调试器之类的东西会改变程序的时间特性。
Billy ONeal

@Billy ONeal:我切换到Release,并且得到了预期的结果。快乐^ _ ^!

11

qsort可能比预期的要好得多的另一个原因是,较新的编译器可以通过函数指针进行内联和优化。

如果C头文件定义了qsort的内联实现而不是在库内部实现,并且编译器支持间接函数内联,则qsort可以与std :: sort一样快。


6

在我的机器上添加一些肉类(将数组变成1000万个元素并在数据部分中移动它)并进行编译

g++ -Wall -O2 -osortspeed sortspeed.cpp

我得到的结果

C quick-sort time elapsed: 3.48
C++ quick-sort time elapsed: 1.26

也要注意可能配置为根据系统负载以可变速度运行的现代“绿色” CPU。进行基准测试时,这种行为会使您发疯(在我的计算机上,我有一个小的脚本可以修复进行速度测试时使用的CPU时钟)。


6
如果您使用性能计数器,“绿色” CPU并不重要(因为您应该这样做才能获得有意义的基准测试结果)
Billy ONeal

性能计数器很棒,但是如果您不尝试测量微小的东西,时钟并不是那么糟糕。另外clock()是按进程的,perf计数器是全局的。
6502

2
@ 6502:您已经颠倒了。性能计数器是每个进程的,时钟是全局的。
Billy ONeal

@Billy ONeal:我以为您的意思是RDTSC,这很好,但具有全球性。不,clock()是每个进程的计数器。见cs.utah.edu/dept/old/texinfo/glibc-manual-0.02/library_19.html
6502

1
@ 6502:glibc!=标准c。通常,我相信这些事情根据实现的rdtsc,但是OS会跟踪执行上下文切换时的时间戳记,并在将上下文提供给要衡量的流程时还原这些值。
Billy ONeal

4

编写准确的基准非常困难,因此让我们让Nonius来为我们做吧!让我们测试qsortstd::sort不进行内联,并且std::sort对一百万个随机整数的向量进行内联(默认设置)。

// sort.cpp
#define NONIUS_RUNNER
#include <nonius.h++>
#include <random>
#include <algorithm>

// qsort
int comp(const void* a, const void* b) {
    const int arg1 = *static_cast<const int*>(a);
    const int arg2 = *static_cast<const int*>(b);

    // we can't simply return a - b, because that might under/overflow
    return (arg1 > arg2) - (arg1 < arg2);
}

// std::sort with no inlining
struct compare_noinline {
    __attribute__((noinline)) bool operator()(const int a, const int b) {
        return a < b;
    }
};

// std::sort with inlining
struct compare {
    // the compiler will automatically inline this
    bool operator()(const int a, const int b) {
        return a < b;
    }
};

std::vector<int> gen_random_vector(const size_t size) {

    std::random_device seed;
    std::default_random_engine engine{seed()};
    std::uniform_int_distribution<int> dist{std::numeric_limits<int>::min(), std::numeric_limits<int>::max()};

    std::vector<int> vec;
    for (size_t i = 0; i < size; i += 1) {
        const int rand_int = dist(engine);
        vec.push_back(rand_int);
    }

    return vec;
}

// generate a vector of a million random integers
constexpr size_t size = 1'000'000;
static const std::vector<int> rand_vec = gen_random_vector(size);

NONIUS_BENCHMARK("qsort", [](nonius::chronometer meter) {

    // Nonius does multiple runs of the benchmark, and each one needs a new
    // copy of the original vector, otherwise we'd just be sorting the same
    // one over and over
    const size_t runs = static_cast<size_t>(meter.runs());
    std::vector<std::vector<int>> vectors{runs};
    std::fill(vectors.begin(), vectors.end(), rand_vec);

    meter.measure([&](const size_t run) {

        std::vector<int>& current_vec = vectors[run];

        std::qsort(current_vec.data(), current_vec.size(), sizeof(int), comp);

        return current_vec;
    });
});

NONIUS_BENCHMARK("std::sort noinline", [](nonius::chronometer meter) {

    const size_t runs = static_cast<size_t>(meter.runs());
    std::vector<std::vector<int>> vectors{runs};
    std::fill(vectors.begin(), vectors.end(), rand_vec);

    meter.measure([&](const size_t run) {

        std::vector<int>& current_vec = vectors[run];

        std::sort(current_vec.begin(), current_vec.end(), compare_noinline{});

        return current_vec;

    });
});

NONIUS_BENCHMARK("std::sort inline", [](nonius::chronometer meter) {

    const size_t runs = static_cast<size_t>(meter.runs());
    std::vector<std::vector<int>> vectors{runs};
    std::fill(vectors.begin(), vectors.end(), rand_vec);

    meter.measure([&](const size_t run) {

        std::vector<int>& current_vec = vectors[run];

        std::sort(current_vec.begin(), current_vec.end(), compare{});

        return current_vec;

    });
});

使用Apple Clang 7.3.0进行编译,

$ clang++ -std=c++14 -stdlib=libc++ -O3 -march=native sort.cpp -o sort
$ ./sort

并在我的1.7 GHz i5 Macbook Air上运行

qsort                211 ms +/- 6 ms
std::sort noinline   127 ms +/- 5 ms
std::sort inline      87 ms +/- 4 ms

因此std::sort,没有内联比qsort(大约是由于排序算法不同)的内联快约1.7倍,而内联凹凸的速度要快约2.4倍。无疑是令人印象深刻的加速,但远低于670%。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.