it-swarm.com.de

Verweist das Übergeben von Argumenten als const auf vorzeitige Optimierung?

"Vorzeitige Optimierung ist die Wurzel allen Übels"

Ich denke, darüber können wir uns alle einig sein. Und ich bemühe mich sehr, das zu vermeiden.

Aber in letzter Zeit habe ich mich gefragt, wie man Parameter an const Reference anstatt an Value übergibt. Mir wurde beigebracht/gelernt, dass nicht triviale Funktionsargumente (dh die meisten nicht primitiven Typen) vorzugsweise von const reference übergeben werden sollten - einige Bücher, die ich gelesen habe, empfehlen dies als "Best Practice" ".

Trotzdem frage ich mich: Moderne Compiler und neue Sprachfunktionen können Wunder wirken. Daher ist das Wissen, das ich gelernt habe, möglicherweise veraltet, und ich habe mich nie darum gekümmert, ein Profil zu erstellen, wenn es Leistungsunterschiede zwischen diesen gibt

void fooByValue(SomeDataStruct data);   

und

void fooByReference(const SomeDataStruct& data);

Ist die Praxis, die ich gelernt habe - Bestehen const Verweise (standardmäßig für nicht triviale Typen) - vorzeitige Optimierung?

23
CharonX

Bei "Vorzeitige Optimierung" geht es nicht darum, Optimierungen zu verwenden früh. Es geht darum, zu optimieren, bevor das Problem verstanden wird, bevor die Laufzeit verstanden wird, und häufig Code für lesbare Ergebnisse weniger lesbar und weniger wartbar zu machen.

Die Verwendung von "const &" anstelle der Übergabe eines Objekts als Wert ist eine gut verstandene Optimierung mit gut verstandenen Auswirkungen auf die Laufzeit, praktisch ohne Aufwand und ohne negative Auswirkungen auf die Lesbarkeit und Wartbarkeit. Es verbessert tatsächlich beide, weil es mir sagt, dass ein Aufruf das übergebene Objekt nicht ändert. Das Hinzufügen von "const &" direkt beim Schreiben des Codes ist also NICHT PREMATURE.

54
gnasher729

TL; DR: Das Übergeben einer konstanten Referenz ist in C++ immer noch eine gute Idee. Keine vorzeitige Optimierung.

TL; DR2: Die meisten Sprichwörter machen keinen Sinn, bis sie es tun.


Ziel

Diese Antwort versucht nur, das verknüpfte Element in den C++ Core Guidelines (zuerst in Amons Kommentar erwähnt) ein wenig zu erweitern.

Diese Antwort versucht nicht, die Frage zu beantworten, wie die verschiedenen Sprichwörter, die in Programmierkreisen weit verbreitet waren, richtig zu denken und anzuwenden sind, insbesondere die Frage der Versöhnung zwischen widersprüchlichen Schlussfolgerungen oder Beweisen.


Anwendbarkeit

Diese Antwort gilt nur für Funktionsaufrufe (nicht abnehmbare verschachtelte Bereiche im selben Thread).

(Randnotiz.) Wenn passable Dinge aus dem Bereich entweichen können (dh eine Lebensdauer haben, die möglicherweise den äußeren Bereich überschreitet), wird es wichtiger, die Anforderungen der Anwendung an die Verwaltung der Objektlebensdauer vor allem anderen zu erfüllen . In der Regel müssen hierfür Referenzen verwendet werden, die auch eine lebenslange Verwaltung ermöglichen, z. B. intelligente Zeiger. Eine Alternative könnte die Verwendung eines Managers sein. Beachten Sie, dass Lambda eine Art abnehmbares Zielfernrohr ist. Lambda-Captures verhalten sich wie Objektbereiche. Seien Sie daher vorsichtig mit Lambda-Aufnahmen. Achten Sie auch darauf, wie das Lambda selbst weitergegeben wird - per Kopie oder als Referenz.


Wann wird der Wert übergeben

Bei Werten, die skalar sind (Standardprimitive, die in ein Maschinenregister passen und eine Wertsemantik haben), für die keine Kommunikation durch Veränderbarkeit erforderlich ist (gemeinsame Referenz), übergeben Sie den Wert.

In Situationen, in denen Angerufene das Klonen eines Objekts oder Aggregats erfordern, übergeben Sie den Wert, in dem die Kopie der Angerufenen die Anforderung eines geklonten Objekts erfüllt.


Wann als Referenz übergeben werden soll usw.

übergeben Sie in allen anderen Situationen Zeiger, Referenzen, intelligente Zeiger, Handles (siehe: Handle-Body-Idiom) usw. Wenden Sie das Prinzip der Konstantenkorrektheit wie gewohnt an, wenn dieser Rat befolgt wird.

Dinge (Aggregate, Objekte, Arrays, Datenstrukturen), deren Speicherbedarf ausreichend groß ist, sollten aus Leistungsgründen immer so gestaltet sein, dass die Referenzübergabe erleichtert wird. Dieser Rat gilt definitiv, wenn es Hunderte von Bytes oder mehr sind. Dieser Rat ist grenzwertig, wenn er mehrere zehn Bytes umfasst.


Ungewöhnliche Paradigmen

Es gibt spezielle Programmierparadigmen, die absichtlich kopierintensiv sind. Beispiel: Zeichenfolgenverarbeitung, Serialisierung, Netzwerkkommunikation, Isolation, Umbruch von Bibliotheken von Drittanbietern, Kommunikation zwischen Prozessen mit gemeinsamem Speicher usw. In diesen Anwendungsbereichen oder Programmierparadigmen werden Daten von Strukturen zu Strukturen kopiert oder manchmal neu verpackt Byte-Arrays.


Wie sich die Sprachspezifikation auf diese Antwort auswirkt, vorher Optimierung wird berücksichtigt.

Sub-TL; DR Die Weitergabe einer Referenz sollte keinen Code aufrufen. Das Bestehen durch const-reference erfüllt dieses Kriterium. Alle anderen Sprachen erfüllen dieses Kriterium jedoch mühelos.

(Anfängern von C++ - Programmierern wird empfohlen, diesen Abschnitt vollständig zu überspringen.)

(Der Anfang dieses Abschnitts ist teilweise von der Antwort von gnasher729 inspiriert. Es wird jedoch eine andere Schlussfolgerung gezogen.)

C++ ermöglicht benutzerdefinierte Kopierkonstruktoren und Zuweisungsoperatoren.

(Dies ist (war) eine mutige Entscheidung, die sowohl erstaunlich als auch bedauerlich ist (war). Es ist definitiv eine Abweichung von der heute akzeptablen Norm im Sprachdesign.)

Selbst wenn der C++ - Programmierer keine definiert, muss der C++ - Compiler solche Methoden basierend auf Sprachprinzipien generieren und dann bestimmen, ob zusätzlicher Code anders als memcpy ausgeführt werden muss. Zum Beispiel muss ein class/struct, das ein std::vector - Mitglied enthält, einen Kopierkonstruktor und einen Zuweisungsoperator haben, der nicht trivial ist.

In anderen Sprachen wird von Kopierkonstruktoren und dem Klonen von Objekten abgeraten (außer wenn dies für die Semantik der Anwendung unbedingt erforderlich und/oder sinnvoll ist), da Objekte aufgrund des Sprachdesigns eine Referenzsemantik aufweisen. Diese Sprachen verfügen normalerweise über einen Speicherbereinigungsmechanismus, der auf Erreichbarkeit anstelle von bereichsbasiertem Besitz oder Referenzzählung basiert.

Wenn eine Referenz oder ein Zeiger (einschließlich der Konstantenreferenz) in C++ (oder C) herumgereicht wird, wird dem Programmierer sichergestellt, dass außer der Weitergabe des Adresswerts kein spezieller Code (benutzerdefinierte oder vom Compiler generierte Funktionen) ausgeführt wird (Referenz oder Zeiger). Dies ist eine Klarheit des Verhaltens, mit der C++ - Programmierer vertraut sind.

Hintergrund ist jedoch, dass die C++ - Sprache unnötig kompliziert ist, so dass diese Klarheit des Verhaltens wie eine Oase (ein überlebensfähiger Lebensraum) irgendwo in der Nähe einer nuklearen Fallout-Zone ist.

Um mehr Segen (oder Beleidigung) hinzuzufügen, führt C++ universelle Referenzen (r-Werte) ein, um benutzerdefinierte Verschiebungsoperatoren (Verschiebungskonstruktoren und Verschiebungszuweisungsoperatoren) mit guter Leistung zu ermöglichen. Dies kommt einem äußerst relevanten Anwendungsfall (dem Verschieben (Übertragen) von Objekten von einer Instanz zu einer anderen) zugute, da weniger Kopieren und Deep-Cloning erforderlich sind. In anderen Sprachen ist es jedoch unlogisch, von einer solchen Bewegung von Objekten zu sprechen.


(Off-Topic-Abschnitt) Ein Abschnitt, der einem Artikel gewidmet ist: "Willst du Geschwindigkeit? Pass by Value!" geschrieben in circa 2009.

Dieser Artikel wurde 2009 geschrieben und erläutert die Entwurfsbegründung für den r-Wert in C++. Dieser Artikel enthält ein gültiges Gegenargument zu meiner Schlussfolgerung im vorherigen Abschnitt. Das Codebeispiel und der Leistungsanspruch des Artikels wurden jedoch lange widerlegt.

Sub-TL; DR Das Design der R-Wert-Semantik in C++ ermöglicht eine überraschend elegante benutzerseitige Semantik für eine Sort -Funktion , beispielsweise. Dieses elegante Modell lässt sich nicht in anderen Sprachen modellieren (imitieren).

Eine Sortierfunktion wird auf eine gesamte Datenstruktur angewendet. Wie oben erwähnt, wäre es langsam, wenn viel kopiert wird. Als Leistungsoptimierung (die praktisch relevant ist) ist eine Sortierfunktion so konzipiert, dass sie in einigen anderen Sprachen als C++ destruktiv ist. Destruktiv bedeutet, dass die Zieldatenstruktur geändert wird, um das Sortierziel zu erreichen.

In C++ kann der Benutzer eine der beiden Implementierungen aufrufen: eine destruktive mit besserer Leistung oder eine normale, die die Eingabe nicht ändert. (Die Vorlage ist der Kürze halber weggelassen.)

/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
    std::vector<T> result(std::move(input)); /* destructive move */
    std::sort(result.begin(), result.end()); /* in-place sorting */
    return result; /* return-value optimization (RVO) */
}

/*caller specifically passes in read-only argument*/ 
std::vector<T> my_sort(const std::vector<T>& input)
{
    /* reuse destructive implementation by letting it work on a clone. */
    /* Several things involved; e.g. expiring temporaries as r-value */
    /* return-value optimization, etc. */
    return my_sort(std::vector<T>(input));  
}

/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/    

Abgesehen von der Sortierung ist diese Eleganz auch bei der Implementierung eines destruktiven Medianfindungsalgorithmus in einem Array (anfangs unsortiert) durch rekursive Partitionierung nützlich.

Beachten Sie jedoch, dass die meisten Sprachen einen ausgeglichenen binären Suchbaumansatz auf die Sortierung anwenden würden, anstatt einen destruktiven Sortieralgorithmus auf Arrays anzuwenden. Daher ist die praktische Relevanz dieser Technik nicht so hoch, wie es scheint.


Wie sich die Compileroptimierung auf diese Antwort auswirkt

Wenn Inlining (und auch die Optimierung des gesamten Programms/der Verbindungszeit) auf mehrere Ebenen von Funktionsaufrufen angewendet wird, kann der Compiler den Datenfluss (manchmal erschöpfend) sehen. In diesem Fall kann der Compiler viele Optimierungen anwenden, von denen einige die Erstellung ganzer Objekte im Speicher verhindern können. Wenn diese Situation zutrifft, spielt es normalerweise keine Rolle, ob die Parameter als Wert oder als Konstantenreferenz übergeben werden, da der Compiler eine umfassende Analyse durchführen kann.

Wenn die Funktion der unteren Ebene jedoch etwas aufruft, das nicht analysiert werden kann (z. B. etwas in einer anderen Bibliothek außerhalb der Kompilierung oder ein einfach zu kompliziertes Aufrufdiagramm), muss der Compiler defensiv optimieren.

Objekte, die größer als ein Maschinenregisterwert sind, können durch explizite Anweisungen zum Laden/Speichern des Speichers oder durch einen Aufruf der ehrwürdigen Funktion memcpy kopiert werden. Auf einigen Plattformen generiert der Compiler SIMD-Befehle, um zwischen zwei Speicherstellen zu wechseln, wobei jeder Befehl mehrere zehn Bytes (16 oder 32) bewegt.


Diskussion zum Thema Ausführlichkeit oder visuelle Unordnung

C++ - Programmierer sind daran gewöhnt, d. H. Solange ein Programmierer C++ nicht hasst, ist der Aufwand für das Schreiben oder Lesen von Konstantenreferenzen im Quellcode nicht schrecklich.

Die Kosten-Nutzen-Analysen wurden möglicherweise schon oft durchgeführt. Ich weiß nicht, ob es wissenschaftliche gibt, die zitiert werden sollten. Ich denke, die meisten Analysen wären nicht wissenschaftlich oder nicht reproduzierbar.

Folgendes stelle ich mir vor (ohne Beweise oder glaubwürdige Referenzen) ...

  • Ja, dies wirkt sich auf die Leistung von in dieser Sprache geschriebener Software aus.
  • Wenn Compiler den Zweck von Code verstehen können, ist er möglicherweise intelligent genug, um dies zu automatisieren
  • Leider würde der Compiler in Sprachen, die die Veränderlichkeit bevorzugen (im Gegensatz zur funktionalen Reinheit), die meisten Dinge als mutiert klassifizieren, daher würde der automatisierte Abzug der Konstanz die meisten Dinge als Nicht-Konstante ablehnen
  • Der mentale Aufwand hängt von den Menschen ab. Leute, die dies als hohen mentalen Aufwand empfinden, hätten C++ als praktikable Programmiersprache abgelehnt.
16
rwong

In DonaldKnuths Artikel "StructuredProgrammingWithGoToStatements" schrieb er: "Programmierer verschwenden enorm viel Zeit damit, über die Geschwindigkeit unkritischer Teile ihrer Programme nachzudenken oder sich darüber Gedanken zu machen, und diese Effizienzversuche wirken sich tatsächlich stark negativ aus, wenn Debugging und Wartung in Betracht gezogen werden Wir sollten kleine Wirkungsgrade vergessen, etwa in 97% der Fälle: Vorzeitige Optimierung ist die Wurzel allen Übels. Dennoch sollten wir unsere Chancen bei diesen kritischen 3% nicht verpassen. " - Vorzeitige Optimierung

Dies rät Programmierern nicht, die langsamsten verfügbaren Techniken zu verwenden. Es geht darum, sich beim Schreiben von Programmen auf Klarheit zu konzentrieren. Klarheit und Effizienz sind oft ein Kompromiss: Wenn Sie nur eine auswählen müssen, wählen Sie Klarheit. Wenn Sie jedoch beides problemlos erreichen können, müssen Sie die Klarheit nicht beeinträchtigen (z. B. signalisieren, dass etwas eine Konstante ist), um Effizienz zu vermeiden.

8
Lawrence

Beim Übergeben von ([const] [rvalue] reference) | (value) sollten die Absichten und Versprechen der Schnittstelle berücksichtigt werden. Es hat nichts mit Leistung zu tun.

Richys Faustregel:

void foo(X x);          // I intend to own the x you gave me, whether by copy, move or direct initialisation on the call stack.     

void foo(X&& x);        // I intend to steal x from you. Do not use it other than to re-assign to it after calling me.

void foo(X const& x);   // I guarantee not to change your x

void foo(X& x);         // I may modify your x and I will leave it in a defined state
8
Richard Hodges

Theoretisch sollte die Antwort ja sein. Tatsächlich ist es ja manchmal so - tatsächlich kann es eine Pessimierung sein, eine konstante Referenz zu übergeben, anstatt nur einen Wert zu übergeben, selbst in Fällen, in denen der übergebene Wert zu groß ist, um in einen einzelnen zu passen registrieren (oder die meisten anderen Heuristiken versuchen zu bestimmen, wann der Wert übergeben werden soll oder nicht). Vor Jahren schrieb David Abrahams einen Artikel mit dem Titel "Willst du Geschwindigkeit? Pass by Value!" einige dieser Fälle abdecken. Es ist nicht mehr leicht zu finden, aber wenn Sie eine Kopie ausgraben können, lohnt es sich, sie zu lesen (IMO).

Im speziellen Fall der Übergabe einer konstanten Referenz würde ich jedoch sagen, dass die Redewendung so gut etabliert ist, dass sich die Situation mehr oder weniger umkehrt: Es sei denn, Sie wissen der Typ ist char/short/int/long, die Leute erwarten, dass es standardmäßig als konstante Referenz übergeben wird, daher ist es wahrscheinlich am besten, dem zu folgen, es sei denn, Sie haben eine ziemlich spezifische Grund, etwas anderes zu tun.

3
Jerry Coffin