Schnelle Schnittmenge der Mengen: C ++ vs C #

8

Auf meinem Computer (Quadcore, 8 GB RAM), der Vista x64 Business mit Visual Studio 2008 SP1 ausführt, versuche ich, zwei Zahlengruppen sehr schnell zu schneiden.

Ich habe zwei Ansätze in C ++ und einen in C # implementiert. Der C # -Ansatz ist bisher schneller, ich möchte den C ++ - Ansatz verbessern, also ist er schneller als C #, was ich von C ++ erwarten kann.

Hier ist die C # -Ausgabe: (Release build)

%Vor%

Hier ist die ursprüngliche C ++ - Ausgabe für zwei verschiedene Ansätze (Release x64 build):

%Vor%

Hier ist die neueste C ++ - Ausgabe für drei Ansätze (Release x64 build):

Neueste Benchmark:

%Vor%

Also ist der set_intersection-Ansatz jetzt ungefähr 2x langsamer als C #, aber 2x schneller als die anfänglichen C ++ - Ansätze.

Letzter C ++ Code:

%Vor%

C # -Code:

%Vor%

C ++ - Code:

%Vor%

Ok, hier ist das Neueste, mit einigen Änderungen:

  • Die C ++ - Sets sind nun richtig eingerichtet, so dass sie eine 50% -Kreuzung haben (wie die C #)
  • Set1 wird gemischt, also ist es nicht sortiert, set2 wurde bereits nicht sortiert
  • Die set_intersection-Implementierung verwendet jetzt Vektoren und sortiert sie zuerst

C ++ (Release, x64) Ergebnisse:

%Vor%

Also ist es 2x langsamer als C #. @Jalf: Du bekommst ein paar ziemlich schnelle Zahlen. Gibt es etwas, das ich hier falsch mache?

C ++ Code:

%Vor%     
Alex Black 29.06.2009, 21:23
quelle

13 Antworten

27

Es gibt mehrere Probleme mit Ihrem Test.

Zuerst testen Sie nicht die Schnittmenge, sondern "erstellen ein paar Felder, füllen sie mit Zufallszahlen und führen dann den Schnittpunkt". Sie sollten nur den Teil des Codes takten, an dem Sie wirklich interessiert sind. Selbst wenn Sie diese Dinge tun wollen, sollten sie hier nicht bewertet werden. Messen Sie eine Sache nach der anderen, um die Unsicherheit zu reduzieren. Wenn Sie möchten, dass Ihre C ++ - Implementierung besser funktioniert, müssen Sie zuerst wissen, welcher Teil langsamer als erwartet ist. Das bedeutet, dass Sie den Setup-Code vom Schnittpunkttest trennen müssen.

Zweitens sollten Sie den Test viele Male ausführen, um mögliche Caching-Effekte und andere Unsicherheiten zu berücksichtigen. (Und wahrscheinlich eine Gesamtzeit für, sagen wir, 1000 Läufe, anstatt einer einzelnen Zeit für jeden ausgeben. Auf diese Weise verringern Sie die Ungewissheit von der Zeitschaltuhr, die Auflösung begrenzt haben könnte und ungenaue Ergebnisse berichten, wenn im 0-20ms-Bereich verwendet / p>

Außerdem, soweit ich aus den Dokumenten lesen kann, sollte die Eingabe in set_intersection sortiert werden, was set2 nicht sein wird. Anscheinend gibt es keinen Grund, unordered_map zu verwenden, wenn unordered_set viel besser für das passt, was Sie tun.

Über den Setup-Code, der benötigt wird, beachten Sie, dass Sie wahrscheinlich nicht brauchen, um Vektoren zu füllen, um die Kreuzung zu führen. Sowohl Ihre eigene Implementierung als auch set_intersection arbeiten bereits an Iteratoren, sodass Sie ihnen einfach ein Paar Iteratoren an die Datenstrukturen übergeben können, in denen Ihre Eingaben bereits enthalten sind.

Ein paar genauere Kommentare zu Ihrem Code:

  • Verwenden Sie ++iterator anstelle von iterator++
  • Anstatt vector.end () bei jeder Schleifeniteration aufzurufen, rufen Sie sie einmal auf und cachen das Ergebnis
  • Experimentieren Sie mit sortierten Vektoren vs Std :: set vs unordered_set (nicht unordered_map )

Bearbeiten:

Ich habe Ihre C # Version nicht ausprobiert, daher kann ich die Zahlen nicht richtig vergleichen, aber hier ist mein modifizierter Test. Jedes wird 1000 Mal auf einem Core 2 Quad 2,5 GHz mit 4 GB RAM ausgeführt:

%Vor%

Der letzte ist ein bisschen unfair, weil er die Vektoren kopieren und sortieren muss. Im Idealfall sollte nur die Sorte Teil des Benchmarks sein. Ich habe versucht, eine Version zu erstellen, die ein Array von 1000 unsortierten Vektoren verwendet (ich würde also nicht die unsortierten Daten in jeder Iteration kopieren müssen), aber die Leistung war ungefähr gleich, oder etwas schlechter, da dies zu konstanten Cache-Fehlern führen würde Also bin ich zu dieser Version zurückgekehrt

Und mein Code:

%Vor%

Es gibt keinen Grund, warum C ++ immer schneller sein sollte als C #. C # hat einige wichtige Vorteile, die mit großer Sorgfalt in C ++ konkurrieren müssen. Der erste, den ich mir vorstellen kann, ist, dass dynamische Zuweisungen im .NET-Land lächerlich billig sind. Jedes Mal, wenn ein C ++ - Vektor, set oder unordered_set (oder ein anderer Container) die Größe ändern oder erweitern muss, ist dies eine sehr kostspielige Operation malloc . In .NET ist eine Heap-Zuweisung wenig mehr als das Hinzufügen eines Offsets zu einem Zeiger.

Wenn also die C ++ - Version konkurrieren soll, müssen Sie wahrscheinlich das Problem lösen, indem Sie Ihren Containern die Größe anpassen, ohne die Heapzuweisungen vornehmen zu müssen, wahrscheinlich mithilfe benutzerdefinierter Zuordnungen für die Container (möglicherweise boost :: pool) eine gute Wette sein, oder Sie können versuchen, Ihre eigenen rollen)

Ein weiteres Problem ist, dass set_difference nur bei sortierten Eingaben funktioniert. Um Testergebnisse zu reproduzieren, die eine Sortierung beinhalten, müssen wir in jeder Iteration eine neue Kopie der unsortierten Daten erstellen, was sehr kostspielig ist. die Verwendung von benutzerdefinierten Zuordnern wird viel helfen). Ich weiß nicht, welche Form Ihre Eingabe nimmt, aber es ist möglich, dass Sie Ihre Eingabe direkt sortieren können, ohne sie zu kopieren, und dann set_difference direkt darauf ausführen. (Das wäre leicht zu tun, wenn Ihre Eingabe zumindest ein Array oder ein STL-Container ist.)

Einer der Hauptvorteile der STL ist, dass sie so flexibel ist, dass sie nahezu jede Eingabesequenz verarbeiten kann. In C # müssen Sie die Eingabe in eine Liste oder ein Dictionary kopieren, aber in C ++ können Sie möglicherweise std::sort und set_intersection auf der unformatierten Eingabe ausführen.

Versuchen Sie schließlich, den Code über einen Profiler auszuführen und genau zu sehen, wo die Zeit verbracht wird. Sie können auch versuchen, den Code stattdessen über GCC auszuführen. Ich habe den Eindruck, dass die STL-Leistung in MSVC manchmal ein bisschen schrullig ist. Es könnte sich lohnen, unter einem anderen Compiler zu testen, nur um zu sehen, ob Sie ähnliche Zeiten dort bekommen.

Schließlich finden Sie diese Blogposts relevant für die Leistung von C ++ vs C #: Ссылка

Die Moral von diesen ist im Wesentlichen, dass ja, Sie können bessere Leistung in C ++ bekommen, aber es ist eine erstaunliche Menge an Arbeit.

    
jalf 30.06.2009, 01:26
quelle
9

Ein Problem, das ich sofort sehe, ist, dass Sie die Mengen in C ++ als Wert und nicht als Konstante übergeben. Sie kopieren sie also jedes Mal, wenn Sie sie weitergeben!

Ich würde auch keinen Satz für das Ziel von set_intersection verwenden. Ich würde etwas wie

verwenden %Vor%

Dieser Code wird jedoch immer noch innerhalb der Funktion zugewiesen. Noch schneller wäre

%Vor%

Und dann Scratch zuweisen, bevor Sie den Timer starten.

Wenn Sie nur nach der Größe suchen, kann eine handgeschriebene for-Schleife in Kombination mit set :: find noch bessere Ergebnisse liefern.

    
rlbond 29.06.2009 22:49
quelle
4

Benutze das ...

%Vor%

... um Vektoren mit einer Anfangsgröße ungleich Null zu erhalten. Verwenden Sie dann nicht push_back, sondern aktualisieren Sie die Werte direkt.

    
Roddy 29.06.2009 21:32
quelle
2

Ich würde den C ++ "runIntersectionTest" ändern, um Const-Referenzen auf die Container zu nehmen, statt sie bei jedem Aufruf kopieren zu lassen. (Der C # -Code verwendet Refs.)

    
Ivan K 29.06.2009 22:55
quelle
2

Es kann sich auch lohnen, den Boost-Container Disjoint Set zu betrachten, welches speziell für bestimmte Arten von großen Mengenoperationen optimiert ist.

Es funktioniert, indem eine Gruppe von Mengen als Vereinigungen mehrerer disjunkte Mengen behandelt wird, was es möglich macht, andere Mengen, wie Kreuzungen oder Vereinigungen sehr billig zu bauen, sobald die ursprüngliche Menge von disjunkten Mengen konstruiert ist. Wenn Sie erwarten, viele Set-Operationen an Sets durchzuführen, die sich nicht viel ändern, können Sie wahrscheinlich erwarten, dass dies sehr schnell geht. Wenn Sie andererseits jedes Set einmal benutzen und wegwerfen, wird es wahrscheinlich nicht zu viel tun.

Wie auch immer, du würdest dir selbst einen Gefallen tun, um wenigstens damit zu experimentieren, um zu sehen, ob es dir in deinem speziellen Fall einen Stoß gibt.

    
quelle
2

Übrigens, wenn Sie große sortierte Mengen haben, ist std :: set_intersection nicht der schnellste Algorithmus. std :: set_intersection benötigt bis zu 2 * (m + n) -1 Vergleiche, aber Algorithmen wie der von Baeza-Yates können schneller sein. Für kleine m ist Baeza-Yates O (m * log (n)), während für n = alpha * m O (n) ist. Die Grundidee besteht darin, eine Art 2-seitige binäre Suche durchzuführen.

Ссылка

Experimentelle Analyse eines schnellen Schnittalgorithmus für sortierte Sequenzen Ricardo Baeza-Yates und Alejandro Salinger

ODER

R. Baeza-Yates. Ein Fast Set Intersection Algorithmus für sortierte Sequenzen. Im Proceedings des 15. jährlichen Symposiums über kombinatorische Pattern Matching (CPM 2004), Springer LNCS 3109, S. 400-408, Istanbul, Türkei, Juli 2004.

Unten finden Sie eine Erklärung und eine Implementierung von Erik Frey, wo er mit einer binären Sonde signifikant schnellere Ergebnisse als mit std :: set_intersection zeigt. Ich habe seinen Code noch nicht ausprobiert.
Ссылка

  1. Wählen Sie das mittlere Element, A, in der kleinerer Satz.
  2. Suchen Sie nach dem Einfügepositionselement B in der größere Satz.
  3. Wenn A und B gleich sind, hängen Sie das Element an Ergebnis.
  4. Wiederholen Sie die Schritte 1-4 für nicht leere Untermengen auf beiden Seiten der Elemente A und B.

;

%Vor%

// probe.hpp

/ ** * binäre Sonde: Wählen Sie das nächste Element, indem Sie den halben Punkt zwischen niedrig und hoch wählen * / Vorlage & lt; Klasse RandomAccessIterator, Klasse T & gt; struct binary_probe {   RandomAccessIterator operator () (RandomAccessIterator begin, RandomAccessIterator end, const T & amp; value)   {     Rückkehr beginnen + ((Ende - Anfang) & gt; & gt; 1);   } };

/ ** * lower_bound: wie stl's lower_bound, aber mit verschiedenen Arten von Sondierungen * Beachten Sie das Aussehen der seltenen Vorlagenvorlage! * / Vorlage & lt; Template-Klasse Probe, Klasse RandomAccessIterator, Klasse T & gt; RandomAccessIterator lower_bound (RandomAccessIterator begin, RandomAccessIterator end, const T & amp; value) {   RandomAccessIterator-Grube;   Sonde & lt; RandomAccessIterator, T & gt; pfunc; // Probe-Funktor (will funktionieren)

while (Anfang & lt; Ende)   {     pit = pfunc (Anfang, Ende, Wert);     if (* pit & lt; Wert)       begin = Grube + 1;     sonst       Ende = Grube;   }   Rückkehr beginnen; }

/ * * diesmal mit einem Komparator! * / Vorlage & lt; Template-Klasse Probe, Klasse RandomAccessIterator, Klasse T, Klasse Comparator & gt; RandomAccessIterator lower_bound (RandomAccessIterator begin, RandomAccessIterator end, const T & amp; value, Comparator cmp) {   RandomAccessIterator-Grube;   Sonde & lt; RandomAccessIterator, T & gt; pfunc;

while (Anfang & lt; Ende)   {     pit = pfunc (Anfang, Ende, Wert);     if (cmp (* pit, Wert))       begin = Grube + 1;     sonst       Ende = Grube;   }   Rückkehr beginnen; }

    
Corwin Joy 26.08.2010 03:38
quelle
1

Da Sie Visual Studio verwenden, sollten Sie überprüfen, ob _SECURE_SCL auf 1 gesetzt ist (normalerweise, wenn Sie es nicht explizit festgelegt haben, ist es 1). Wenn es gesetzt ist, wird der gesamte STL-Code auch in Release-Builds range-geprüft. In der Regel wird der Code um 10-15% verlangsamt.

Es scheint, dass Microsoft nicht wusste, dass zum Beispiel std :: vector bereits eine Schnittstelle hat, wenn Sie die Bereichsüberprüfung wünschen: std :: vector :: at ()!

(Entschuldige, ich musste es von meiner Brust nehmen).

Wie auch immer, die größte Ineffizienz ist, dass Sie die Container kopieren, anstatt sie nach Wert zu übergeben. Verwenden Sie Referenzen zu (versuchen Sie es) vergleichen Sie Äpfel und Äpfel statt Äpfel und Bananen.

    
Andreas Magnusson 29.06.2009 22:57
quelle
0

Ich weiß, dass Ihre Lösung gut funktioniert, aber Sie haben versucht, die STL-Implementierungen zu verwenden:

Es könnte bereits für Ihre Platform optimiert sein, also würde ich es versuchen

    
Edison Gustavo Muenz 29.06.2009 21:48
quelle
0

Sind C ++ Optimierungsflags aktiviert?

    
Magnus 29.06.2009 22:31
quelle
0

Ok, nach vielen Rückmeldungen habe ich die ursprüngliche Frage mehrmals aktualisiert:

  • Die Tests werden jetzt jeweils 1.000 mal ausgeführt
  • Der C # -Code verwendet jetzt einen Timer mit höherer Auflösung
  • Die Datenstrukturen werden jetzt vor den Tests
  • gefüllt

Das Ergebnis davon ist, dass C # immer noch ~ 5x schneller ist als C ++.

Vielen Dank für Ihre Ideen / Vorschläge.

    
Alex Black 29.06.2009 22:32
quelle
0

Aktualisierung:

Ich habe den set_intersection-Code modifiziert, um Vektoren zu verwenden, und um sie zu sortieren (anstatt die sortierte Mengenklasse zu verwenden), und jetzt ist es VIEL schneller:

%Vor%

Beachten Sie: Der größere Satz wird sortiert erstellt, sodass die Sortierung in diesem Beispiel nicht viel Zeit in Anspruch nehmen kann.

C ++ Code:

%Vor%     
Alex Black 29.06.2009 22:50
quelle
0

Sie übergeben die Vektoren immer noch nach Wert. Was wäre in Ordnung, wenn Sie sie nicht auch kopieren würden.

Inserter hat die Werte nicht am Ende des Vektors platziert, wo es schnell ist. Es tat dies nur bei der ersten Einfügung, danach fügte es den Wert am Anfang des Arrays ein (wo das Ende verwendet wurde).

Sie haben den Wert in der Hash-Map-Version zweimal nachgeschlagen, als Sie den Wert aktualisiert haben. Warum wird dieses Wert-Ereignis aktualisiert?

Führe diesen Code aus und poste deine Timings.

%Vor%     
deft_code 30.06.2009 00:35
quelle
0

Neueste Benchmark:

%Vor%

Ich denke, der Unterschied zwischen 504 und 495 tritt auf, weil es ein paar Duplikatwerte gibt.

%Vor%     
Alex Black 30.06.2009 00:55
quelle

Tags und Links