【问题标题】:Performance of qsort vs std::sort?qsort 与 std::sort 的性能?
【发布时间】:2011-06-10 03:15:43
【问题描述】:

根据 Scott Meyers 在其 Effective STL 书 - 第 46 项中的说法。他声称 std::sortstd::qsort 快约 670%,因为它是内联的。我测试了自己,发现 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 第 3 版 ( 2001 )
第 7 章 STL 编程
第 46 条:考虑函数对象而不是函数作为算法参数。

【问题讨论】:

  • 你让你的编译器优化了吗?调试/未优化的构建不会充分利用内联等功能。
  • 了解快速排序的工作原理,会让您更好地了解如何测试它,简而言之: 1. 使用更大的数组,例如:10^6 大小,然后以降序填充数组order 999999... 4,3,2,1 - 这将导致排序变为 O(n^2),这样做将有效地证明为什么内联比较器会在这个特定算法中产生如此大的差异。
  • @Zenikoder- 几乎没有qsortsort 的实现会使用在反向排序输入上中断的快速排序实现。最常见的 STL sort 实现使用 introsort,它对快速排序例程进行内省,以确保它永远不会降级到比 O(n lg n) 更差,而且我相当有信心 C qsort 例程使用类似的东西(或至少有一个启发式方法,比如三中位数)来防止这种情况发生。
  • @Noah:根据 06 年关于 artima SM 的一篇文章:“我将从你们中的许多人会发现的一个不可挽回的忏悔开始:我已经 20 多年没有编写生产软件了,而且我从未用 C++ 编写过生产软件。”他称自己是 C++ 语言的考古学家/人类学家。
  • @Chan:本特利和麦克罗伊的论文可以在这里找到:cs.ubc.ca/local/reading/proceedings/spe91-95/spe/vol23/issue11/…

标签: c++ performance sorting stl


【解决方案1】:

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

最后,我复制并粘贴了你的代码,qsort 得到 0.016,std::sort 得到 0.008。

【讨论】:

  • @DeadMG:谢谢!我更改为发布模式,我得到了类似的结果。我真的很喜欢 Scott Meyers,我相信他的话 ;)
  • 文本似乎是两种情况下的输出,因此它不能完全使结果无效。
  • @Oo Tiib:正在输出的文本并不意味着它不会同时输出。如果缓冲区比第一个大但小于第二个怎么办?现在它必须在第二次调用之前刷新 - 但在第一次调用中没有。哦亲爱的。不过我不是很高兴,因为我解决了上述所有问题,并且 qsort 现在快了很多。 :(
  • @DeadMG:qsort 怎么快很多?你能解释一下吗?
  • @DeadMG: std::qsort 要求“此函数的返回值应通过分别返回负值、零或正值。” operator&lt; 不满足该要求(特别是它只返回 0 或 1)。检查以确保 std::sortstd::qsort 在您的测试中产生相同的结果 :) (只需将 - 更改为 &lt; 会导致 qsort 为我返回错误的答案)
【解决方案2】:

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

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

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

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

【讨论】:

  • 我很惊讶没有人提到任何衡量代码实际有效性的策略。您可以编写一个对几百个元素进行排序的小程序,将所有内容加载到您的 L1 缓存中,并在创纪录的时间内完成它,但这绝不会反映您在具有数百个其他元素的系统上运行的实际程序活动进程,进行上下文切换,因为您受计算限制并且调度程序讨厌您,同时对新泽西州大小的数据集进行排序。让您的基准测试看起来更像真正的应用程序。
【解决方案3】:

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

【讨论】:

  • 谢谢!我用的是Visual Studio,实在不知道怎么开启优化。
  • @Chan:切换到使用“发布”版本。还要确保不要从 Visual Studio 中运行程序来进行基准测试——调试器之类的东西会改变程序的时间特征。
  • @Billy ONeal:我切换到 Release,得到了预期的结果。快乐^_^!
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

    在我的机器上添加一些肉(使数组包含 1000 万个元素并将其移动到数据部分)并编译

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

    我得到了结果

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

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

    【讨论】:

    • “绿色”CPU 在您使用性能计数器时并不重要(因为您应该这样做以获得有意义的基准测试结果)
    • 性能计数器很棒,但如果您不尝试测量小东西,时钟也不会那么糟糕。此外,clock() 是每个进程的,性能计数器是全局的。
    • @6502:你把它反过来了。性能计数器是每个进程的,时钟是全局的。
    • @Billy ONeal:我以为您的意思是 RDTSC,这非常好,但具有全球性。不,clock() 是每个进程的计数器。见cs.utah.edu/dept/old/texinfo/glibc-manual-0.02/library_19.html
    • @6502: glibc != 标准 c。通常我相信这些东西根据rdtsc实现的,但是操作系统在执行上下文切换时会跟踪时间戳是什么,并在上下文返回给正在测量的过程。
    【解决方案6】:

    编写准确的基准很困难,所以让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::sortqsort 快大约 1.7 倍(可能是由于不同的排序算法),而内联则快了大约 2.4 倍。当然是令人印象深刻的加速,但远低于 670%。

    【讨论】:

    • 这可能是 CPU 缓存值的情况,因此对它们的连续访问要快得多,因此,例如,如果您将在 qsort 之前运行 std::sort 测试,它会显示 qsort 更快?
    【解决方案7】:

    为什么在 C 标准库的比较函数中没有人提到额外的内存获取?

    在C标准库中,void*用于保存所有类型的成员数据,这意味着当实际访问成员数据时,必须对void*指针进行一次额外的解引用。

    struct list {
            void *p_data; // deference this pointer when access real data
            struct list *prev;
            struct list *next;
    };
    

    但是,在STL中,借助模板的代码生成能力,C标准库中用void*保存的成员数据可以直接放在类型内部,避免了访问过程中额外的解引用。

    template <typename T>
    class list {
    public:
            T data; // access data directly
            list *prev;
            list *next;
    };
    

    所以,理论上,std::sort 比 qsort 快。

    【讨论】:

      【解决方案8】:

      我不确定快 670%。它一定是为显示std::sort 的速度而量身定制的特定数据集。一般来说,std::sort 确实比 qsort 快,因为以下几点:

      1. qsortvoid* 进行操作,首先需要取消引用,其次需要数据类型的大小来执行交换。因此,qsort 的交换操作是每个字节进行的。查看qsort 实现并注意它的SWAP 宏是一个循环。这是 Jon Bentley 解释时间差异的视频(从 45m 开始):https://www.youtube.com/watch?v=aMnn0Jq0J-E&t=2700s

      2. inline 可能会使它加快一点,但那是微优化,不是主要贡献者。

      3. std::sort实际上是一种混合算法,称为Introsort。 C qsort 是一个纯粹的快速排序实现。给定一个不适合快速排序的数据集,std::sort 改为堆排序。因此,如果您为 qsort 创建了一个错误的输入,它将会非常缓慢。

      4. 上面的分析代码是不够的。应增加输入大小。 100K 是不够的。将其增加到 1M 或 10M,然后重复多次排序,然后取平均值或中位数。如有必要,将它们编译成单独的二进制文件并单独运行。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-01-09
        • 2015-04-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-23
        • 2015-06-20
        相关资源
        最近更新 更多