Kurzversion: Es ist üblich, große Objekte wie Vektoren / Arrays in vielen Programmiersprachen zurückzugeben. Ist dieser Stil jetzt in C ++ 0x akzeptabel, wenn die Klasse einen Verschiebungskonstruktor hat, oder halten C ++ - Programmierer ihn für seltsam / hässlich / abscheulich?
Lange Version: Wird dies in C ++ 0x immer noch als schlechte Form angesehen?
std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();
Die traditionelle Version würde so aussehen:
void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);
In der neueren Version ist der zurückgegebene Wert BuildLargeVector
ein r- Wert , daher würde v mit dem Verschiebungskonstruktor von erstellt std::vector
, vorausgesetzt, (N) RVO findet nicht statt.
Selbst vor C ++ 0x war die erste Form aufgrund von (N) RVO oft "effizient". (N) RVO liegt jedoch im Ermessen des Compilers. Jetzt, da wir rWert-Referenzen haben, ist garantiert, dass keine tiefe Kopie stattfindet.
Edit : Bei der Frage geht es wirklich nicht um Optimierung. Beide gezeigten Formen weisen in realen Programmen eine nahezu identische Leistung auf. Während in der Vergangenheit die erste Form eine um eine Größenordnung schlechtere Leistung hätte erzielen können. Infolgedessen war die erste Form lange Zeit ein Hauptcodegeruch in der C ++ - Programmierung. Nicht mehr, hoffe ich?
Antworten:
Dave Abrahams hat eine ziemlich umfassende Analyse der Geschwindigkeit der Übergabe / Rückgabe von Werten .
Kurze Antwort: Wenn Sie einen Wert zurückgeben müssen, geben Sie einen Wert zurück. Verwenden Sie keine Ausgabereferenzen, da der Compiler dies trotzdem tut. Natürlich gibt es Vorbehalte, deshalb sollten Sie diesen Artikel lesen.
quelle
x / 2
umx >> 1
fürint
s, aber Sie davon ausgehen , es wird. Der Standard sagt auch nichts darüber aus, wie Compiler Referenzen implementieren müssen, aber Sie gehen davon aus, dass sie mithilfe von Zeigern effizient gehandhabt werden. Der Standard sagt auch nichts über V-Tabellen aus, sodass Sie nicht sicher sein können, ob virtuelle Funktionsaufrufe auch effizient sind. Im Wesentlichen müssen Sie manchmal etwas Vertrauen in den Compiler setzen.Zumindest IMO, es ist normalerweise eine schlechte Idee, aber nicht aus Effizienzgründen. Dies ist eine schlechte Idee, da die betreffende Funktion normalerweise als generischer Algorithmus geschrieben werden sollte, der ihre Ausgabe über einen Iterator erzeugt. Fast jeder Code, der einen Container akzeptiert oder zurückgibt, anstatt mit Iteratoren zu arbeiten, sollte als verdächtig angesehen werden.
Verstehen Sie mich nicht falsch: Manchmal ist es sinnvoll, sammlungsähnliche Objekte (z. B. Zeichenfolgen) weiterzugeben, aber für das angeführte Beispiel würde ich die Übergabe oder Rückgabe des Vektors als schlechte Idee betrachten.
quelle
Das Wesentliche ist:
Copy Elision und RVO können die "beängstigenden Kopien" vermeiden (der Compiler muss diese Optimierungen nicht implementieren und kann in einigen Situationen nicht angewendet werden).
C ++ 0x RValue-Referenzen ermöglichen eine String / Vektor-Implementierung, die dies garantiert .
Wenn Sie ältere Compiler / STL-Implementierungen aufgeben können, geben Sie Vektoren frei zurück (und stellen Sie sicher, dass auch Ihre eigenen Objekte dies unterstützen). Wenn Ihre Codebasis "kleinere" Compiler unterstützen muss, halten Sie sich an den alten Stil.
Leider hat dies großen Einfluss auf Ihre Schnittstellen. Wenn C ++ 0x keine Option ist und Sie Garantien benötigen, können Sie in einigen Szenarien stattdessen Objekte mit Referenzzählung oder Copy-on-Write verwenden. Sie haben jedoch Nachteile beim Multithreading.
(Ich wünschte, nur eine Antwort in C ++ wäre einfach und unkompliziert und ohne Bedingungen).
quelle
Tatsächlich, da C ++ 11, die Kosten für das Kopieren der
std::vector
in den meisten Fällen verschwunden.Beachten Sie jedoch, dass die Kosten für die Erstellung des neuen Vektors (und die anschließende Zerstörung ) weiterhin bestehen und die Verwendung von Ausgabeparametern anstelle der Rückgabe nach Wert weiterhin nützlich ist, wenn Sie die Kapazität des Vektors wiederverwenden möchten. Dies ist als Ausnahme in F.20 der C ++ - Kernrichtlinien dokumentiert .
Lass uns vergleichen:
mit:
Angenommen, wir müssen diese Methoden
numIter
mal in einer engen Schleife aufrufen und eine Aktion ausführen. Berechnen wir zum Beispiel die Summe aller Elemente.Mit
BuildLargeVector1
würden Sie tun:Mit
BuildLargeVector2
würden Sie tun:Im ersten Beispiel treten viele unnötige dynamische Zuweisungen / Freigaben auf, die im zweiten Beispiel verhindert werden, indem ein Ausgabeparameter auf die alte Weise verwendet wird und bereits zugewiesener Speicher wiederverwendet wird. Ob sich diese Optimierung lohnt oder nicht, hängt von den relativen Kosten der Zuweisung / Freigabe im Vergleich zu den Kosten für die Berechnung / Mutation der Werte ab.
Benchmark
Spielen wir mit den Werten von
vecSize
undnumIter
. Wir werden vecSize * numIter konstant halten, so dass "theoretisch" dieselbe Zeit benötigt wird (= es gibt dieselbe Anzahl von Zuweisungen und Ergänzungen mit genau denselben Werten) und der Zeitunterschied nur aus den Kosten von resultieren kann Zuweisungen, Freigaben und bessere Verwendung des Cache.Verwenden wir insbesondere vecSize * numIter = 2 ^ 31 = 2147483648, da ich 16 GB RAM habe und diese Nummer sicherstellt, dass nicht mehr als 8 GB zugewiesen werden (sizeof (int) = 4), um sicherzustellen, dass ich nicht auf die Festplatte wechsle ( Alle anderen Programme waren geschlossen, ich hatte ~ 15 GB zur Verfügung, als ich den Test ausführte.
Hier ist der Code:
Und hier ist das Ergebnis:
(Intel i7-7700K bei 4,20 GHz; 16 GB DDR4 2400 MHz; Kubuntu 18.04)
Notation: mem (v) = v.size () * sizeof (int) = v.size () * 4 auf meiner Plattform.
Es überrascht nicht, wann
numIter = 1
die Zeiten (dh mem (v) = 8 GB), vollkommen identisch sind. In beiden Fällen weisen wir nur einmal einen riesigen Vektor von 8 GB Speicher zu. Dies beweist auch, dass bei Verwendung von BuildLargeVector1 () keine Kopie stattgefunden hat: Ich hätte nicht genug RAM, um die Kopie zu erstellen!Wann
numIter = 2
Wiederverwendung der Vektorkapazität anstelle der erneuten Zuweisung eines zweiten Vektors 1,37-mal schneller ist.Wenn die
numIter = 256
Wiederverwendung der Vektorkapazität (anstatt einen Vektor 256 Mal immer wieder zuzuweisen / freizugeben ...) 2,45x schneller ist :)Wir können feststellen , dass time1 ziemlich konstant ist , von
numIter = 1
zunumIter = 256
, was bedeutet , dass eine große Vektor von 8 GB Zuteilung ziemlich viel ist so teuer , wie die Zuteilung 256 Vektoren von 32 MB. Das Zuweisen eines großen Vektors mit 8 GB ist jedoch definitiv teurer als das Zuweisen eines Vektors mit 32 MB. Die Wiederverwendung der Kapazität des Vektors führt also zu Leistungssteigerungen.Von
numIter = 512
(mem (v) = 16 MB) bisnumIter = 8M
(mem (v) = 1 KB) ist der Sweet Spot: Beide Methoden sind genau so schnell und schneller als alle anderen Kombinationen von numIter und vecSize. Dies hat wahrscheinlich damit zu tun, dass die L3-Cache-Größe meines Prozessors 8 MB beträgt, sodass der Vektor so gut wie vollständig in den Cache passt. Ich erkläre nicht wirklich, warum der plötzliche Sprung vontime1
für mem (v) = 16 MB ist. Es scheint logischer, kurz danach zu geschehen, wenn mem (v) = 8 MB. Beachten Sie, dass an diesem Sweet Spot die Nichtwiederverwendung von Kapazität überraschenderweise etwas schneller ist! Ich erkläre das nicht wirklich.Wenn
numIter > 8M
es hässlich wird. Beide Methoden werden langsamer, aber die Rückgabe des Vektors nach Wert wird noch langsamer. Im schlimmsten Fall ist bei einem Vektor, der nur einen einzigen enthältint
, die Wiederverwendungskapazität anstelle der Rückgabe nach Wert 3,3-mal schneller. Vermutlich liegt dies an den Fixkosten von malloc (), die zu dominieren beginnen.Beachten Sie, wie die Kurve für Zeit2 glatter ist als die Kurve für Zeit1: Nicht nur die Wiederverwendung der Vektorkapazität ist im Allgemeinen schneller, sondern möglicherweise noch wichtiger, sie ist vorhersehbarer .
Beachten Sie auch, dass wir im Sweet Spot 2 Milliarden Additionen von 64-Bit-Ganzzahlen in ~ 0,5 Sekunden durchführen konnten, was auf einem 4,2-GHz-64-Bit-Prozessor durchaus optimal ist. Wir könnten es besser machen, indem wir die Berechnung parallelisieren, um alle 8 Kerne zu verwenden (der obige Test verwendet jeweils nur einen Kern, was ich durch erneutes Ausführen des Tests während der Überwachung der CPU-Auslastung überprüft habe). Die beste Leistung wird erzielt, wenn mem (v) = 16 kB ist, was der Größenordnung des L1-Cache entspricht (der L1-Datencache für den i7-7700K beträgt 4 x 32 kB).
Natürlich werden die Unterschiede immer weniger relevant, je mehr Berechnungen Sie tatsächlich für die Daten durchführen müssen. Unten sind die Ergebnisse, wenn wir ersetzen
sum = std::accumulate(v.begin(), v.end(), sum);
durchfor (int k : v) sum += std::sqrt(2.0*k);
:Schlussfolgerungen
Die Ergebnisse können auf anderen Plattformen abweichen. Wenn es auf die Leistung ankommt, schreiben Sie wie gewohnt Benchmarks für Ihren speziellen Anwendungsfall.
quelle
Ich denke immer noch, dass es eine schlechte Praxis ist, aber es ist erwähnenswert, dass mein Team MSVC 2008 und GCC 4.1 verwendet, sodass wir nicht die neuesten Compiler verwenden.
Zuvor waren viele der in vtune mit MSVC 2008 angezeigten Hotspots auf das Kopieren von Zeichenfolgen zurückzuführen. Wir hatten Code wie diesen:
... beachten Sie, dass wir unseren eigenen String-Typ verwendet haben (dies war erforderlich, da wir ein Software-Entwicklungskit bereitstellen, in dem Plugin-Writer unterschiedliche Compiler und damit unterschiedliche, inkompatible Implementierungen von std :: string / std :: wstring verwenden können).
Ich habe eine einfache Änderung in Reaktion auf die Sitzung zur Erstellung von Stichprobenprofilen für Anrufdiagramme vorgenommen, die zeigt, dass String :: String (const String &) viel Zeit in Anspruch nimmt. Methoden wie im obigen Beispiel leisteten die größten Beiträge (tatsächlich zeigte die Profilerstellungssitzung, dass die Speicherzuweisung und -freigabe einer der größten Hotspots ist, wobei der String-Kopierkonstruktor der Hauptbeitrag für die Zuweisungen ist).
Die Änderung, die ich vorgenommen habe, war einfach:
Dies machte jedoch einen großen Unterschied! Der Hotspot wurde in nachfolgenden Profiler-Sitzungen entfernt. Darüber hinaus führen wir zahlreiche gründliche Unit-Tests durch, um die Leistung unserer Anwendung zu verfolgen. Alle Arten von Leistungstestzeiten sind nach diesen einfachen Änderungen erheblich gesunken.
Fazit: Wir verwenden nicht die absolut neuesten Compiler, aber wir können uns immer noch nicht darauf verlassen, dass der Compiler das Kopieren für eine zuverlässige Rückgabe nach Wert optimiert (zumindest nicht in allen Fällen). Dies ist möglicherweise nicht der Fall für diejenigen, die neuere Compiler wie MSVC 2010 verwenden. Ich freue mich darauf, wenn wir C ++ 0x verwenden und einfach rvalue-Referenzen verwenden können und uns nie Sorgen machen müssen, dass wir unseren Code durch die Rückgabe von Komplex pessimieren Klassen nach Wert.
[Bearbeiten] Wie Nate betonte, gilt RVO für die Rückgabe von Temporären, die innerhalb einer Funktion erstellt wurden. In meinem Fall gab es keine solchen Provisorien (mit Ausnahme des ungültigen Zweigs, in dem wir eine leere Zeichenfolge erstellen), und daher wäre RVO nicht anwendbar gewesen.
quelle
<::
oder??!
mit dem bedingten Operator?:
(manchmal auch als " Operator" bezeichnet) ternärer Operator bezeichnet ).Nur um ein wenig zu picken: In vielen Programmiersprachen ist es nicht üblich, Arrays von Funktionen zurückzugeben. In den meisten von ihnen wird ein Verweis auf ein Array zurückgegeben. In C ++ würde die nächste Analogie zurückkehren
boost::shared_array
quelle
shared_ptr
und nennen Sie ihn einen Tag.Wenn die Leistung ein echtes Problem darstellt, sollten Sie sich darüber im Klaren sein, dass die Verschiebungssemantik nicht immer schneller ist als das Kopieren. Wenn Sie beispielsweise eine Zeichenfolge haben, die die Optimierung für kleine Zeichenfolgen verwendet, muss ein Verschiebungskonstruktor für kleine Zeichenfolgen genau so viel Arbeit leisten wie ein regulärer Kopierkonstruktor.
quelle