web-dev-qa-db-fra.com

Performances de qsort vs std :: sort?

Selon Scott Meyers, dans son livre Effective STL - point 46. Il a affirmé que std::sort est environ 670% plus rapide que std::qsort en raison du fait de l'inline. Je me suis testé et j'ai vu que qsort est plus rapide :(! Quelqu'un pourrait-il m'aider à expliquer ce comportement étrange?

#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";
}

Voici mon résultat:

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

Mise à jour

Efficace STL 3e édition (2001)
Chapitre 7 Programmation avec STL
Point 46: Considérez les objets fonction au lieu des fonctions comme paramètres d'algorithme.

Meilleures salutations,

68
Chan

std :: clock () n'est pas une horloge de synchronisation viable. Vous devez utiliser un minuteur de résolution supérieure spécifique à la plate-forme, comme le minuteur de haute performance de Windows. Plus que cela, la façon dont vous appelez clock () est que le texte est d'abord envoyé à la console, qui est inclus dans le temps. Cela invalide définitivement le test. De plus, assurez-vous d'avoir compilé avec toutes les optimisations.

Enfin, j'ai copié et collé votre code, et obtenu 0,016 pour qsort et 0,008 pour std :: sort.

94
Puppy

Je suis surpris que personne ne mentionne de caches.

Dans votre code, vous commencez par toucher ary et * ary_copy * afin qu'ils résident dans le cache au moment de qsort. Pendant qsort, * ary_copy * pourrait être expulsé. Au moment de std :: sort, les éléments devraient être récupérés de la mémoire ou d'un niveau de cache plus grand (lire plus lent). Cela dépendra bien sûr de la taille de votre cache.

Essayez d'inverser le test, c'est-à-dire commencez par exécuter std :: sort.

Comme certains l'ont souligné; agrandir le tableau rendra le test plus équitable. La raison en est qu'un grand tableau est moins susceptible de tenir dans le cache.

18
rasmus

Les deux algorithmes de tri, sans optimisation activée, devraient avoir des performances comparables. La raison pour laquelle le C++ sort a tendance à battre sensiblement qsort est que le compilateur peut aligner les comparaisons en cours, car le compilateur possède des informations de type sur la fonction utilisée pour effectuer la comparaison. Avez-vous exécuté ces tests avec l'optimisation activée? Si ce n'est pas le cas, essayez de l'activer et de relancer ce test.

12
templatetypedef

Une autre raison pour laquelle qsort peut fonctionner beaucoup mieux que prévu est que les nouveaux compilateurs peuvent être intégrés et optimisés via le pointeur de fonction.

Si l'en-tête C définit une implémentation en ligne de qsort au lieu de l'implémenter à l'intérieur d'une bibliothèque et que le compilateur prend en charge l'inline de fonction indirecte, alors qsort peut être aussi rapide que std :: sort.

10
Zan Lynx

Sur ma machine, ajouter de la viande (ce qui rend le tableau 10 millions d'éléments et le déplacer dans la section des données) et compiler avec

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

J'obtiens comme résultat

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

Faites également attention aux processeurs modernes "verts" qui peuvent être configurés pour fonctionner à une vitesse variable en fonction de la charge du système. Lorsque l'analyse comparative de ce type de comportement peut vous rendre fou (sur ma machine, j'ai configuré deux petits scripts normal et fast que je peux utiliser lors des tests de vitesse).

4
6502

Il est difficile d'écrire des benchmarks précis, alors essayons Nonius de le faire pour nous! Testons qsort, std::sort sans incrustation et std::sort avec incrustation (par défaut) sur un vecteur d'un million d'entiers aléatoires.

// 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;

    });
});

Compilation avec Apple Clang 7.3.0,

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

et l'exécuter sur mon Macbook Air 1,7 GHz, nous obtenons

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

Alors std::sort sans incrustation est environ 1,7 fois plus rapide que qsort (peut-être en raison de différents algorithmes de tri), et l'inclinaison des bosses jusqu'à 2,4 fois plus rapide. Une accélération certes impressionnante, mais bien inférieure à 670%.

3
Mr Bingley