Производительность qsort vs std:: sort?


согласно Скотту Мейерсу, в его эффективной книге 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 3-е издание ( 2001 )
Глава 7 Программирование с помощью STL
Пункт 46: рассмотреть объекты функций вместо функций в качестве параметров алгоритма.

С уважением,

7 67

7 ответов:

std:: clock () не является жизнеспособным хронометром. Вы должны использовать специфичный для платформы таймер более высокого разрешения, например таймер высокой производительности Windows. Более того, способ вызова clock() заключается в том, что сначала текст выводится на консоль, которая включена во время. Это определенно делает тест недействительным. Кроме того, убедитесь, что вы скомпилированы со всеми оптимизациями.

наконец, я скопировал и вставил ваш код, и получил 0.016 для qsort и 0.008 для std::sort.

Я удивлен, что никто не упоминает тайники.

в вашем коде, вы начинаете с прикосновения ary и * ary_copy * таким образом, они находятся в кэше в момент qsort. Во время qsort, * ary_copy * может быть выселен. Во время std:: sort, элементы должны быть извлечены из памяти или больше (читать медленнее) уровень кэша. Это, конечно, будет зависеть от ваших размеров кэша.

попробуйте обратный тест, т. е. начните с запуска std:: sort.

как указывали некоторые люди; увеличение массива сделает тест более справедливым. Причина в том, что большой массив с меньшей вероятностью поместится в кэш.

два алгоритма сортировки, без включенной оптимизации, должны иметь сопоставимую производительность. Причина в том, что C++ sort имеет тенденцию заметно бить qsort заключается в том, что компилятор может встроить выполняемые сравнения, поскольку компилятор имеет информацию о типе о том, какая функция используется для выполнения сравнения. Вы запускали эти тесты с включенной оптимизацией? Если нет, попробуйте включить его и снова запустить этот тест.

еще одна причина, по которой qsort может работать намного лучше, чем ожидалось, заключается в том, что новые компиляторы могут встроить и оптимизировать с помощью указателя функции.

Если заголовок C определяет встроенную реализацию qsort вместо реализации ее внутри библиотеки, а компилятор поддерживает косвенную функцию встраивания, то qsort может быть так же быстро, как std::sort.

на моей машине добавляя немного мяса (делая массив 10 миллионов элементов и перемещая его в раздел данных) и компилируя с

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

Я получаю в результате

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

будьте также осторожны с современными" зелеными " процессорами, которые могут быть настроены на работу с переменной скоростью в зависимости от нагрузки на систему. При бенчмаркинге такого рода поведение может свести вас с ума (на моей машине я установил два небольших скрипта normal и fast что я могу использовать для скорости тесты.)

писать точные ориентиры сложно, поэтому давайте получим Нониус сделать это для нас! Давайте проверим qsort,std::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 без встраивания примерно в 1,7 раза быстрее, чем qsort (возможно, из-за различных алгоритмы сортировки), и встраивание ударов, которые примерно в 2,4 раза быстрее. Безусловно, впечатляющее ускорение, но гораздо меньше, чем 670%.

Не знаю, как std:: sort был реализован несколько лет назад. Но std::sort может быть намного быстрее, потому что std:: sort-это quicksort с резервным вариантом сортировки кучи. Heapsort-это linearhitmic alghoritm, что означает, что если у вас есть дважды данные сортировки, время сортировки удваивается. На самом деле он более чем удваивается, потому что он не совсем линейный, но, тем не менее, qsort может быть квадратичным, поэтому требуется экспоненциальное больше времени для сортировки в два раза ввода.