Leistung von qsort vs std :: sort?

74

Laut Scott Meyers in seinem Buch Effective STL - Punkt 46. Er behauptete, dass dies std::sortetwa 670% schneller ist als std::qsortaufgrund der Tatsache der Inline. Ich habe mich selbst getestet und festgestellt, dass qsort schneller ist :(! Könnte mir jemand helfen, dieses seltsame Verhalten zu erklären?

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

Das ist mein Ergebnis:

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

Aktualisieren

Effektive STL 3rd Edition (2001)
Kapitel 7 Programmieren mit STL
Punkt 46: Betrachten Sie Funktionsobjekte anstelle von Funktionen als Algorithmusparameter.

Freundliche Grüße,

Chan
quelle
16
Haben Sie Ihren Compiler optimieren lassen? Debuggen / nicht optimierte Builds nutzen Dinge wie Inlining nicht voll aus.
Edward Strange
5
Wenn Sie verstehen, wie das schnelle Sortieren funktioniert, erhalten Sie eine bessere Vorstellung davon, wie Sie es testen können, kurz: 1. Verwenden Sie ein größeres Array, z. B. 10 ^ 6, und füllen Sie das Array dann in absteigender Reihenfolge 999999 ... 4,3 , 2,1 - dies führt dazu, dass die Sortierung zu O (n ^ 2) wird. Dies zeigt effektiv, warum Inlining-Komparatoren bei diesem speziellen Algorithmus einen so großen Unterschied machen.
10
@ Zenikoder- Fast keine Implementierungen von qsortoder sortwerden eine Quicksort-Implementierung verwenden, die bei umgekehrt sortierten Eingaben unterbrochen wird. Die gebräuchlichste STL- sortImplementierung verwendet Introsort, das die Quicksort-Routine überprüft, um sicherzustellen, dass sie niemals schlechter als O (n lg n) wird, und ich bin ziemlich sicher, dass die C- qsortRoutine etwas Ähnliches (oder zumindest eine Heuristik wie den Median) verwendet -von-drei) um dies zu verhindern.
Templatetypedef
3
@Noah: In einem 06-Artikel über artima SM heißt es: "Ich beginne mit dem, was viele von Ihnen als unwiderruflich schädliches Geständnis empfinden: Ich habe seit über 20 Jahren keine Produktionssoftware mehr geschrieben, und ich habe nie Produktionssoftware in C ++ geschrieben. "" Er nennt sich Archäologe / Anthropologe der C ++ - Sprache.
3
@Chan: Das Papier von Bentley und McIlroy finden Sie hier: cs.ubc.ca/local/reading/proceedings/spe91-95/spe/vol23/issue11/…

Antworten:

98

std :: clock () ist keine brauchbare Zeitschaltuhr. Sie sollten einen plattformspezifischen Timer mit höherer Auflösung verwenden, z. B. den Windows-Hochleistungstimer. Darüber hinaus wird beim Aufrufen von clock () zunächst Text an die Konsole ausgegeben, die in der Uhrzeit enthalten ist. Dies macht den Test definitiv ungültig. Stellen Sie außerdem sicher, dass Sie mit allen Optimierungen kompiliert haben.

Schließlich habe ich Ihren Code kopiert und eingefügt und 0,016 für qsort und 0,008 für std :: sort erhalten.

Hündchen
quelle
1
@DeadMG: Danke! Ich bin in den Release-Modus gewechselt und habe das gleiche Ergebnis erzielt. Ich liebe Scott Meyers wirklich und vertraue seinem Wort;)
Chan
In beiden Fällen wird anscheinend Text ausgegeben, sodass das Ergebnis nicht genau ungültig werden kann.
Öö Tiib
1
@Oo Tiib: Text, der ausgegeben wird, bedeutet nicht, dass er nicht gleichzeitig ausgegeben wird. Was ist, wenn der Puffer etwas größer als der erste, aber kleiner als der zweite ist? Jetzt muss es vor dem zweiten Anruf gespült werden - beim ersten Anruf jedoch nicht. Ach je. Ich bin allerdings nicht sehr glücklich, weil ich alle oben genannten Probleme behoben habe und qsort jetzt viel schneller ist. :(
Welpe
@DeadMG: Wie ist qsort viel schneller? Könnten Sie das erklären?
Chan
1
@DeadMG: std::qsorterfordert "Der Rückgabewert dieser Funktion sollte darstellen, ob elem1 als kleiner, gleich oder größer als elem2 angesehen wird, indem jeweils ein negativer Wert, Null oder ein positiver Wert zurückgegeben wird." operator<erfüllt diese Anforderung nicht (insbesondere wird nur 0 oder 1 zurückgegeben). Überprüfen Sie, ob dies der Fall ist, std::sortund std::qsorterzielen Sie die gleichen Ergebnisse bei Ihren Tests :) (Ändern Sie einfach -die <Ergebnisse qsort, um die falsche Antwort für mich zurückzugeben)
Billy ONeal
20

Ich bin überrascht, dass niemand Caches erwähnt.

In Ihrem Code berühren Sie zunächst ary und * ary_copy *, damit sie sich zum Zeitpunkt von qsort im Cache befinden . Während qsort wird * ary_copy * möglicherweise entfernt. Zum Zeitpunkt von std :: sort müssten die Elemente aus dem Speicher oder einer größeren ( langsamer gelesenen ) Cache-Ebene abgerufen werden. Dies hängt natürlich von Ihrer Cache-Größe ab.

Versuchen Sie, den Test umzukehren, indem Sie zunächst std :: sort ausführen .

Wie einige Leute betont haben; Wenn Sie das Array vergrößern, wird der Test fairer. Der Grund ist, dass ein großes Array weniger wahrscheinlich in den Cache passt.

Rasmus
quelle
5
Ich bin überrascht, dass niemand Strategien zur Messung der tatsächlichen Wirksamkeit des Codes erwähnt hat. Sie können ein winziges Programm schreiben, das ein paar hundert Elemente sortiert, alles in Ihren L1-Cache lädt und in Rekordzeit durchläuft, aber das wird in keiner Weise Ihr tatsächliches Programm widerspiegeln, das auf einem System mit ein paar hundert anderen ausgeführt wird aktive Prozesse, Kontextwechsel, weil Sie rechengebunden sind und der Scheduler Sie hasst, während Sie einen Datensatz von der Größe New Jerseys sortieren. Lassen Sie Ihren Benchmark der realen Anwendung sehr ähnlich sehen.
Wexxor
13

Die beiden Sortieralgorithmen sollten ohne aktivierte Optimierungen eine vergleichbare Leistung aufweisen. Der Grund, warum C ++ sortdazu neigt, merklich zu schlagen, qsortist, dass der Compiler die durchgeführten Vergleiche einbinden kann, da der Compiler Typinformationen darüber hat, welche Funktion zum Durchführen des Vergleichs verwendet wird. Haben Sie diese Tests mit aktivierter Optimierung durchgeführt? Wenn nicht, schalten Sie es ein und führen Sie diesen Test erneut aus.

templatetypedef
quelle
Vielen Dank! Ich verwende Visual Studio und weiß wirklich nicht, wie ich die Optimierung aktivieren soll.
Chan
3
@Chan: Wechseln Sie zur Verwendung des Builds "Release". Stellen Sie außerdem sicher, dass Sie das Programm nicht von innerhalb von Visual Studio für Ihre Benchmarks ausführen - Dinge wie Debugger ändern die Zeitmerkmale Ihres Programms.
Billy ONeal
@ Billy ONeal: Ich bin zu Release gewechselt und habe das erwartete Ergebnis erhalten. Glücklich ^ _ ^!
Chan
11

Ein weiterer Grund dafür, dass qsort möglicherweise eine viel bessere Leistung als erwartet erbringt, besteht darin, dass neuere Compiler über den Funktionszeiger inline und optimiert werden können.

Wenn der C-Header eine Inline-Implementierung von qsort definiert, anstatt sie in einer Bibliothek zu implementieren, und der Compiler das Inlining indirekter Funktionen unterstützt, kann qsort genauso schnell sein wie std :: sort.

Zan Lynx
quelle
6

Auf meiner Maschine etwas Fleisch hinzufügen (das Array 10 Millionen Elemente machen und es im Datenbereich verschieben) und mit kompilieren

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

Ich bekomme als Ergebnis

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

Seien Sie auch vorsichtig mit modernen "grünen" CPUs, die je nach Auslastung des Systems so konfiguriert werden können, dass sie mit variabler Geschwindigkeit laufen. Beim Benchmarking kann diese Art von Verhalten Sie verrückt machen (auf meinem Computer habe ich ein kleines Skript, das die CPU-Uhr korrigiert, die ich bei Geschwindigkeitstests verwende).

6502
quelle
6
"Grüne" CPUs spielen keine Rolle, wenn Sie Leistungsindikatoren verwenden (wie Sie es für aussagekräftige Benchmark-Ergebnisse tun sollten)
Billy ONeal
Leistungsindikatoren sind großartig, aber die Uhr ist nicht so schlecht, wenn Sie nicht versuchen, kleine Dinge zu messen. Auch clock () ist pro Prozess, Leistungsindikatoren sind global.
6502
2
@ 6502: Du hast das umgekehrt. Perf Zähler sind pro Prozess, Uhr ist global.
Billy ONeal
@ Billy ONeal: Ich dachte du meinst RDTSC und das ist sehr schön, aber global. Und nein, clock () ist ein Pro-Prozess-Zähler. Siehe cs.utah.edu/dept/old/texinfo/glibc-manual-0.02/library_19.html
6502
1
@ 6502: glibc! = Standard c. Normalerweise glaube ich , diese Dinge werden umgesetzt in Bezug auf rdtsc, aber das Betriebssystem verfolgt, was die Zeitstempel sind , wenn es einen Kontextwechsel durchführt, und stellt diese Werte , wenn der Kontext gegeben wird dem Prozess wieder gemessen wird.
Billy ONeal
3

Es ist schwierig, genaue Benchmarks zu schreiben. Lassen Sie uns Nonius dazu bringen, dies für uns zu tun! Lassen Sie uns Test qsort, std::sortohne inlining und std::sortmit inlining (Standardeinstellung) auf einen Vektor von einer Million Zufallszahlen.

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

    });
});

Kompilieren mit Apple Clang 7.3.0,

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

und wenn wir es auf meinem 1,7 GHz i5 Macbook Air laufen lassen, bekommen wir

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

Also std::sortohne inlining ist etwa 1,7x schneller als qsort(vielleicht aufgrund unterschiedlicher Sortieralgorithmen) und inlining Beulen , dass bis zu etwa 2,4 - fach schneller. Sicherlich eine beeindruckende Beschleunigung, aber viel weniger als 670%.

Herr Bingley
quelle