Ist es in C ++ immer noch eine schlechte Praxis, einen Vektor von einer Funktion zurückzugeben?

102

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 BuildLargeVectorein 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?

Nate
quelle
18
Wer hat jemals gesagt, dass es von Anfang an eine schlechte Form war?
Edward Strange
7
Es war sicherlich ein schlechter Code-Geruch in den "alten Tagen", aus denen ich komme. :-)
Nate
1
Ich hoffe doch! Ich würde gerne sehen, dass Pass-by-Value immer beliebter wird. :)
sellibitze

Antworten:

73

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.

Peter Alexander
quelle
24
"Compiler macht es trotzdem": Der Compiler muss das nicht tun == Unsicherheit == schlechte Idee (braucht 100% Sicherheit). "Umfassende Analyse" Bei dieser Analyse gibt es ein großes Problem: Sie basiert auf undokumentierten / nicht standardmäßigen Sprachfunktionen in einem unbekannten Compiler ("Obwohl der Standard niemals eine Kopierentscheidung vorschreibt"). Selbst wenn es funktioniert, ist es keine gute Idee, es zu verwenden - es gibt absolut keine Garantie dafür, dass es wie beabsichtigt funktioniert, und es gibt keine Garantie dafür, dass jeder Compiler immer so funktioniert. Sich auf dieses Dokument zu verlassen, ist eine schlechte Codierungspraxis, IMO. Auch wenn Sie an Leistung verlieren.
SigTerm
5
@SigTerm: Das ist ein ausgezeichneter Kommentar !!! Der größte Teil des Artikels, auf den verwiesen wird, ist zu vage, um ihn überhaupt für die Verwendung in der Produktion in Betracht zu ziehen. Die Leute denken, dass alles, was ein Autor, der ein Red In-Depth-Buch geschrieben hat, ein Evangelium ist und ohne weitere Überlegungen oder Analysen eingehalten werden sollte. Am Geldautomaten gibt es keinen Compiler auf dem Markt, der so unterschiedliche Kopien bietet wie die Beispiele, die Abrahams in diesem Artikel verwendet.
Hippicoder
13
@SigTerm, es gibt eine Menge , die der Compiler nicht tun muss, aber Sie gehen davon aus, dass dies trotzdem der Fall ist. Compiler sind nicht „erforderlich“ zu ändern , x / 2um x >> 1für ints, 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.
Peter Alexander
16
@Sig: Es wird nur sehr wenig garantiert, außer der tatsächlichen Ausgabe Ihres Programms. Wenn Sie 100% ige Sicherheit darüber wünschen, was 100% der Zeit passieren wird, sollten Sie sofort zu einer anderen Sprache wechseln.
Dennis Zickefoose
6
@SigTerm: Ich arbeite am "Ist-Szenario". Ich teste, was der Compiler macht und arbeite damit. Es gibt kein "kann langsamer arbeiten". Es funktioniert einfach nicht langsamer, weil der Compiler RVO implementiert, unabhängig davon, ob der Standard dies erfordert oder nicht. Es gibt kein Wenn, Aber oder Vielleicht, es ist nur eine einfache Tatsache.
Peter Alexander
37

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.

Jerry Sarg
quelle
6
Das Problem beim Iterator-Ansatz besteht darin, dass Sie Funktionen und Methoden als Vorlagen erstellen müssen, selbst wenn der Typ des Auflistungselements bekannt ist. Dies ist irritierend und wenn die fragliche Methode virtuell ist, unmöglich. Beachten Sie, dass ich mit Ihrer Antwort per se nicht einverstanden bin, aber in der Praxis wird sie in C ++ nur etwas umständlich.
Jon-Hanson
22
Ich muss nicht zustimmen. Die Verwendung von Iteratoren für die Ausgabe ist manchmal angemessen. Wenn Sie jedoch keinen generischen Algorithmus schreiben, bieten generische Lösungen häufig unvermeidbaren Overhead, der schwer zu rechtfertigen ist. Sowohl in Bezug auf die Codekomplexität als auch auf die tatsächliche Leistung.
Dennis Zickefoose
1
@Dennis: Ich muss sagen, dass meine Erfahrung genau das Gegenteil war: Ich schreibe eine ganze Reihe von Dingen als Vorlagen, auch wenn ich die beteiligten Typen im Voraus kenne, weil dies einfacher ist und die Leistung verbessert.
Jerry Coffin
9
Ich persönlich gebe einen Container zurück. Die Absicht ist klar, der Code ist einfacher, die Leistung beim Schreiben ist mir egal (ich vermeide nur eine frühzeitige Pessimisierung). Ich bin mir nicht sicher, ob die Verwendung eines Ausgabe-Iterators meine Absicht klarer machen würde ... und ich benötige so viel Nicht-Vorlagen-Code wie möglich, da in einem großen Projekt Abhängigkeiten die Entwicklung töten.
Matthieu M.
1
@ Tennis: Ich gehe davon aus, dass Sie konzeptionell niemals "einen Container bauen sollten, anstatt in einen Bereich zu schreiben". Ein Container ist genau das - ein Container. Ihr Anliegen (und das Anliegen Ihres Codes) sollte der Inhalt sein, nicht der Container.
Jerry Coffin
18

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).

peterchen
quelle
11

Tatsächlich, da C ++ 11, die Kosten für das Kopieren der std::vectorin 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:

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

mit:

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

Angenommen, wir müssen diese Methoden numItermal in einer engen Schleife aufrufen und eine Aktion ausführen. Berechnen wir zum Beispiel die Summe aller Elemente.

Mit BuildLargeVector1würden Sie tun:

size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
    std::vector<int> v = BuildLargeVector1(vecSize);
    sum1 = std::accumulate(v.begin(), v.end(), sum1);
}

Mit BuildLargeVector2würden Sie tun:

size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
    BuildLargeVector2(/*out*/ v, vecSize);
    sum2 = std::accumulate(v.begin(), v.end(), sum2);
}

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 vecSizeund numIter. 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:

#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

class Timer {
    using clock = std::chrono::steady_clock;
    using seconds = std::chrono::duration<double>;
    clock::time_point t_;

public:
    void tic() { t_ = clock::now(); }
    double toc() const { return seconds(clock::now() - t_).count(); }
};

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

int main() {
    Timer t;

    size_t vecSize = size_t(1) << 31;
    size_t numIter = 1;

    std::cout << std::setw(10) << "vecSize" << ", "
              << std::setw(10) << "numIter" << ", "
              << std::setw(10) << "time1" << ", "
              << std::setw(10) << "time2" << ", "
              << std::setw(10) << "sum1" << ", "
              << std::setw(10) << "sum2" << "\n";

    while (vecSize > 0) {

        t.tic();
        size_t sum1 = 0;
        {
            for (int i = 0; i < numIter; ++i) {
                std::vector<int> v = BuildLargeVector1(vecSize);
                sum1 = std::accumulate(v.begin(), v.end(), sum1);
            }
        }
        double time1 = t.toc();

        t.tic();
        size_t sum2 = 0;
        {
            std::vector<int> v;
            for (int i = 0; i < numIter; ++i) {
                BuildLargeVector2(/*out*/ v, vecSize);
                sum2 = std::accumulate(v.begin(), v.end(), sum2);
            }
        } // deallocate v
        double time2 = t.toc();

        std::cout << std::setw(10) << vecSize << ", "
                  << std::setw(10) << numIter << ", "
                  << std::setw(10) << std::fixed << time1 << ", "
                  << std::setw(10) << std::fixed << time2 << ", "
                  << std::setw(10) << sum1 << ", "
                  << std::setw(10) << sum2 << "\n";

        vecSize /= 2;
        numIter *= 2;
    }

    return 0;
}

Und hier ist das Ergebnis:

$ g++ -std=c++11 -O3 main.cpp && ./a.out
   vecSize,    numIter,      time1,      time2,       sum1,       sum2
2147483648,          1,   2.360384,   2.356355, 2147483648, 2147483648
1073741824,          2,   2.365807,   1.732609, 2147483648, 2147483648
 536870912,          4,   2.373231,   1.420104, 2147483648, 2147483648
 268435456,          8,   2.383480,   1.261789, 2147483648, 2147483648
 134217728,         16,   2.395904,   1.179340, 2147483648, 2147483648
  67108864,         32,   2.408513,   1.131662, 2147483648, 2147483648
  33554432,         64,   2.416114,   1.097719, 2147483648, 2147483648
  16777216,        128,   2.431061,   1.060238, 2147483648, 2147483648
   8388608,        256,   2.448200,   0.998743, 2147483648, 2147483648
   4194304,        512,   0.884540,   0.875196, 2147483648, 2147483648
   2097152,       1024,   0.712911,   0.716124, 2147483648, 2147483648
   1048576,       2048,   0.552157,   0.603028, 2147483648, 2147483648
    524288,       4096,   0.549749,   0.602881, 2147483648, 2147483648
    262144,       8192,   0.547767,   0.604248, 2147483648, 2147483648
    131072,      16384,   0.537548,   0.603802, 2147483648, 2147483648
     65536,      32768,   0.524037,   0.600768, 2147483648, 2147483648
     32768,      65536,   0.526727,   0.598521, 2147483648, 2147483648
     16384,     131072,   0.515227,   0.599254, 2147483648, 2147483648
      8192,     262144,   0.540541,   0.600642, 2147483648, 2147483648
      4096,     524288,   0.495638,   0.603396, 2147483648, 2147483648
      2048,    1048576,   0.512905,   0.609594, 2147483648, 2147483648
      1024,    2097152,   0.548257,   0.622393, 2147483648, 2147483648
       512,    4194304,   0.616906,   0.647442, 2147483648, 2147483648
       256,    8388608,   0.571628,   0.629563, 2147483648, 2147483648
       128,   16777216,   0.846666,   0.657051, 2147483648, 2147483648
        64,   33554432,   0.853286,   0.724897, 2147483648, 2147483648
        32,   67108864,   1.232520,   0.851337, 2147483648, 2147483648
        16,  134217728,   1.982755,   1.079628, 2147483648, 2147483648
         8,  268435456,   3.483588,   1.673199, 2147483648, 2147483648
         4,  536870912,   5.724022,   2.150334, 2147483648, 2147483648
         2, 1073741824,  10.285453,   3.583777, 2147483648, 2147483648
         1, 2147483648,  20.552860,   6.214054, 2147483648, 2147483648

Benchmark-Ergebnisse

(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 = 256Wiederverwendung 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 = 1zu numIter = 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) bis numIter = 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 von time1fü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 > 8Mes 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ält int, 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);durch for (int k : v) sum += std::sqrt(2.0*k);:

Benchmark 2

Schlussfolgerungen

  1. Die Verwendung von Ausgabeparametern anstelle der Rückgabe nach Wert kann zu Leistungssteigerungen führen, indem die Kapazität wiederverwendet wird.
  2. Auf einem modernen Desktop-Computer scheint dies nur für große Vektoren (> 16 MB) und kleine Vektoren (<1 KB) zu gelten.
  3. Vermeiden Sie die Zuweisung von Millionen / Milliarden kleiner Vektoren (<1 kB). Wenn möglich, verwenden Sie die Kapazität wieder oder, noch besser, gestalten Sie Ihre Architektur anders.

Die Ergebnisse können auf anderen Plattformen abweichen. Wenn es auf die Leistung ankommt, schreiben Sie wie gewohnt Benchmarks für Ihren speziellen Anwendungsfall.

Boris Dalstein
quelle
6

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:

String Something::id() const
{
    return valid() ? m_id: "";
}

... 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:

static String null_string;
const String& Something::id() const
{
    return valid() ? m_id: null_string;
}

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.

stinky472
quelle
3
Das ist die Sache: RVO ist vom Compiler abhängig, aber ein C ++ 0x-Compiler muss die Verschiebungssemantik verwenden, wenn er sich gegen die Verwendung von RVO entscheidet (vorausgesetzt, es gibt einen Verschiebungskonstruktor). Mit dem Trigraph-Operator wird RVO besiegt. Siehe cpp-next.com/archive/2009/09/move-it-with-rvalue-references, auf die sich Peter bezog. Ihr Beispiel ist jedoch ohnehin nicht für die Verschiebungssemantik geeignet, da Sie kein temporäres Objekt zurückgeben.
Nate
@ Stinky472: Die Rückgabe eines Mitglieds nach Wert war immer langsamer als die Referenz. R-Wert-Referenzen wären immer noch langsamer als die Rückgabe einer Referenz an das ursprüngliche Mitglied (wenn der Anrufer eine Referenz nehmen kann, anstatt eine Kopie zu benötigen). Darüber hinaus gibt es noch viele Male, die Sie über rWert-Referenzen speichern können, weil Sie Kontext haben. Sie können beispielsweise String newstring ausführen. newstring.resize (string1.size () + string2.size () + ...); newstring + = string1; newstring + = string2; usw. Dies ist immer noch eine erhebliche Einsparung gegenüber den Werten.
Welpe
@DeadMG eine erhebliche Einsparung gegenüber Binäroperator + auch mit C ++ 0x-Compilern, die RVO implementieren? Wenn ja, ist das eine Schande. Andererseits ist dies sinnvoll, da wir immer noch eine temporäre Datei erstellen müssen, um die verkettete Zeichenfolge zu berechnen, während + = direkt mit der neuen Zeichenfolge verknüpft werden kann.
stinky472
Wie wäre es mit einem Fall wie: string newstr = str1 + str2; Auf einem Compiler, der die Verschiebungssemantik implementiert, sollte dies genauso schnell oder sogar schneller sein als: string newstr; newstr + = str1; newstr + = str2; Sozusagen keine Reserve (ich gehe davon aus, dass Sie Reserve statt Größenänderung gemeint haben).
stinky472
5
@Nate: Ich denke, Sie verwechseln Trigraphen wie <::oder ??!mit dem bedingten Operator ?: (manchmal auch als " Operator" bezeichnet) ternärer Operator bezeichnet ).
Fredoverflow
3

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ückkehrenboost::shared_array

Nemanja Trifunovic
quelle
4
@ Billy: std :: vector ist ein Werttyp mit Kopiersemantik. Der aktuelle C ++ - Standard bietet keine Garantie dafür, dass (N) RVO jemals angewendet wird, und in der Praxis gibt es viele reale Szenarien, wenn dies nicht der Fall ist.
Nemanja Trifunovic
3
@ Billy: Auch hier gibt es einige sehr reale Szenarien, in denen selbst die neuesten Compiler NRVO nicht anwenden: efnetcpp.org/wiki/Return_value_optimization#Named_RVO
Nemanja Trifunovic
3
@ Billy ONeal: 99% ist nicht genug, du brauchst 100%. Murphys Gesetz - "Wenn etwas schief gehen kann, wird es". Unsicherheit ist in Ordnung, wenn Sie mit einer Art Fuzzy-Logik zu tun haben, aber es ist keine gute Idee, traditionelle Software zu schreiben. Wenn es sogar 1% der Wahrscheinlichkeit gibt, dass Code nicht so funktioniert, wie Sie denken, sollten Sie damit rechnen, dass dieser Code einen kritischen Fehler verursacht, der Sie entlassen wird. Außerdem ist es keine Standardfunktion. Die Verwendung von undokumentierten Funktionen ist eine schlechte Idee. Wenn der Compiler in einem Jahr die Funktion fallen lässt (dies ist standardmäßig nicht erforderlich , oder?), Sind Sie in Schwierigkeiten.
SigTerm
4
@SigTerm: Wenn wir über die Richtigkeit des Verhaltens sprechen würden, würde ich Ihnen zustimmen. Wir sprechen jedoch von einer Leistungsoptimierung. Solche Dinge sind mit weniger als 100% iger Sicherheit in Ordnung.
Billy ONeal
2
@Nemanja: Ich sehe nicht, worauf man sich hier "verlässt". Ihre App läuft gleich, egal ob RVO oder NRVO verwendet wird. Wenn sie jedoch verwendet werden, läuft es schneller. Wenn Ihre App auf einer bestimmten Plattform zu langsam ist und Sie sie auf das Kopieren von Rückgabewerten zurückgeführt haben, ändern Sie sie auf jeden Fall. Dies ändert jedoch nichts an der Tatsache, dass die beste Vorgehensweise darin besteht, den Rückgabewert weiterhin zu verwenden. Wenn Sie unbedingt sicherstellen müssen, dass kein Kopieren erfolgt, wickeln Sie den Vektor in a ein shared_ptrund nennen Sie ihn einen Tag.
Billy ONeal
2

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.

Motti
quelle
1
NRVO verschwindet nicht, nur weil Verschiebungskonstruktoren hinzugefügt wurden.
Billy ONeal
1
@ Billy, wahr, aber irrelevant, die Frage war, ob C ++ 0x die Best Practices geändert hat und NRVO sich aufgrund von C ++ 0x nicht geändert hat
Motti