Verweist die Übergabe von Argumenten als const auf eine vorzeitige Optimierung?

20

"Vorzeitige Optimierung ist die Wurzel allen Übels"

Ich denke, wir können uns alle darauf einigen. Und ich bemühe mich sehr, das zu vermeiden.

Aber in letzter Zeit habe ich mich gefragt, wie man Parameter nicht nach Wert, sondern nach Konstantenreferenz übergibt . Mir wurde beigebracht / beigebracht, dass nicht-triviale Funktionsargumente (dh die meisten nicht-primitiven Typen) vorzugsweise als const-Referenz übergeben werden sollten. Einige Bücher, die ich gelesen habe, empfehlen dies als "Best Practice".

Trotzdem kann ich mich nur wundern: Moderne Compiler und neue Sprachfunktionen können Wunder wirken, so dass das Wissen, das ich gelernt habe, sehr wohl veraltet sein kann, und ich habe mich nie darum gekümmert, ein Profil zu erstellen, wenn es Leistungsunterschiede gibt

void fooByValue(SomeDataStruct data);   

und

void fooByReference(const SomeDataStruct& data);

Ist die Übung, die ich gelernt habe - Übergeben von Konstantenreferenzen (standardmäßig für nicht triviale Typen) - vorzeitige Optimierung?

CharonX
quelle
1
Siehe auch: F.call in den C ++ Core Guidelines für eine Diskussion verschiedener Parameterübergabestrategien.
Amon
1
@DocBrown Die akzeptierte Antwort auf diese Frage bezieht sich auf das Prinzip des geringsten Erstaunens , das auch hier gelten kann (dh die Verwendung von const-Referenzen ist Industriestandard usw. usw.). Ich bin jedoch anderer Meinung, dass es sich bei der Frage um ein Duplikat handelt: Bei der Frage, auf die Sie sich beziehen, wird gefragt, ob es eine schlechte Praxis ist, sich (im Allgemeinen) auf die Compileroptimierung zu verlassen. Diese Frage stellt die umgekehrte Frage: Handelt es sich bei der Übergabe von Konstantenreferenzen um eine (vorzeitige) Optimierung?
CharonX
@CharonX: Wenn man sich hier auf die Compileroptimierung verlassen kann, lautet die Antwort auf Ihre Frage eindeutig "Ja, die manuelle Optimierung ist nicht notwendig, sie ist verfrüht". Wenn man sich nicht darauf verlassen kann (vielleicht weil man vorher nicht weiß, welche Compiler jemals für den Code verwendet werden), lautet die Antwort "für größere Objekte ist es wahrscheinlich nicht verfrüht". Selbst wenn diese beiden Fragen nicht buchstäblich gleich sind, scheinen sie IMHO ähnlich genug zu sein, um sie als Duplikate miteinander zu verknüpfen.
Doc Brown
1
@DocBrown: Bevor Sie es als Dupe deklarieren können, weisen Sie darauf hin, wo in der Frage steht, dass der Compiler dies "optimieren" darf und kann.
Deduplizierer

Antworten:

49

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

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

gnasher729
quelle
2
Ich stimme dem "praktisch mühelosen" Teil Ihrer Antwort zu. Bei der vorzeitigen Optimierung geht es jedoch in erster Linie um die Optimierung, bevor eine merkliche, gemessene Auswirkung auf die Leistung erzielt wird. Und ich denke nicht, dass die meisten C ++ - Programmierer (zu denen auch ich selbst gehört) vor der Verwendung irgendwelche Messungen vornehmen const&, daher halte ich die Frage für vernünftig.
Doc Brown
1
Sie messen vor dem Optimieren, ob sich Kompromisse lohnen. Mit const & ist der gesamte Aufwand sieben Zeichen zu tippen, und es hat andere Vorteile. Wenn Sie die übergebene Variable nicht ändern möchten, ist dies auch dann von Vorteil, wenn keine Geschwindigkeitsverbesserung erfolgt.
gnasher729
3
Ich bin kein C-Experte, also eine Frage: const& foosagt, dass die Funktion foo nicht ändert, so dass der Anrufer sicher ist. Ein kopierter Wert besagt jedoch, dass kein anderer Thread foo ändern kann, sodass der Angerufene in Sicherheit ist. Richtig? In einer Multithread-App hängt die Antwort von der Richtigkeit und nicht von der Optimierung ab.
user949300
1
@DocBrown Sie können eventuell die Motivation des Entwicklers, der die const &? Wenn er es nur für die Leistung tat, ohne den Rest zu berücksichtigen, könnte es als vorzeitige Optimierung angesehen werden. Wenn er es jetzt so formuliert, weil er weiß, dass es sich um einen konstanten Parameter handelt, dokumentiert er seinen Code nur selbst und gibt dem Compiler die Möglichkeit, ihn zu optimieren, was besser ist.
Walfrat
1
@ user949300: Mit wenigen Funktionen können ihre Argumente gleichzeitig oder durch verwendete Rückrufe geändert werden. Dies wird ausdrücklich gesagt.
Deduplicator
16

TL; DR: In C ++ ist es immer noch eine gute Idee, einen konstanten Verweis zu übergeben. 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 ++ - Grundrichtlinien zu erweitern (zuerst in amons Kommentar erwähnt) ein wenig zu erweitern.

Diese Antwort versucht nicht, die Frage zu beantworten, wie man die verschiedenen Sprichwörter, die in den Kreisen der Programmierer weit verbreitet waren, richtig denkt und anwendet, insbesondere die Frage der Vereinbarkeit zwischen widersprüchlichen Schlussfolgerungen oder Beweisen.


Anwendbarkeit

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

(Randnotiz.) Wenn passierbare Dinge aus dem Geltungsbereich herausfallen können (dh eine Lebensdauer haben, die möglicherweise den äußeren Geltungsbereich überschreitet), wird es wichtiger, die Anforderungen der Anwendung an die Verwaltung der Objektlebensdauer vor allen anderen zu erfüllen. Dies erfordert normalerweise die Verwendung von Referenzen, die auch lebenslang verwaltet werden können, z. B. intelligente Zeiger. Eine Alternative könnte die Verwendung eines Managers sein. Beachten Sie, dass Lambda eine Art abnehmbarer Bereich ist. Lambda-Erfassungen verhalten sich wie Objekterfassungen. Seien Sie daher vorsichtig mit Lambda-Erfassungen. Achten Sie auch darauf, wie das Lambda selbst weitergegeben wird - als Kopie oder als Referenz.


Wann soll der Wert übergeben werden?

Bei skalaren Werten (Standardprimitiven, die in ein Maschinenregister passen und eine Wertesemantik aufweisen), für die keine Kommunikation durch Mutabilität (gemeinsame Referenz) erforderlich ist, übergeben Sie den Wert.

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


Wann soll man als Referenz übergeben, etc.

Übergeben Sie in allen anderen Situationen Zeiger, Verweise, intelligente Zeiger, Handles (siehe: Handle-Body-Idiom) usw. Wenden Sie das Prinzip der konstanten Korrektheit wie gewohnt an, wenn dieser Hinweis befolgt wird.

Dinge (Aggregate, Objekte, Arrays, Datenstrukturen), die einen ausreichend großen Speicherbedarf haben, sollten aus Leistungsgründen immer so entworfen werden, dass das Pass-by-Reference erleichtert wird. Dieser Hinweis gilt auf jeden Fall, wenn es sich um Hunderte von Bytes oder mehr handelt. Dieser Rat ist grenzwertig, wenn es sich um zehn Bytes handelt.


Ungewöhnliche Paradigmen

Es gibt spezielle Programmierparadigmen, die absichtlich kopierintensiv sind. Beispielsweise String-Verarbeitung, Serialisierung, Netzwerkkommunikation, Isolation, Wrapping 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 gepackt Byte-Arrays.


Wie sich die Sprachspezifikation auf diese Antwort auswirkt, bevor die Optimierung in Betracht gezogen wird.

Sub-TL; DR Bei der Weitergabe einer Referenz sollte kein Code aufgerufen werden. Das Übergeben von const-reference erfüllt dieses Kriterium. Alle anderen Sprachen erfüllen dieses Kriterium jedoch mühelos.

(Anfängern von C ++ 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 Wahl, die sowohl erstaunlich als auch bedauerlich ist (war). Es ist definitiv eine Abweichung von der heute akzeptablen Norm im Sprachdesign.)

Auch wenn der C ++ - Programmierer keine definiert, muss der C ++ - Compiler solche Methoden basierend auf Sprachprinzipien generieren und dann bestimmen, ob zusätzlicher Code ausgeführt werden muss memcpy. Zum Beispiel enthält a class/ structdas astd::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 (sofern dies für die Semantik der Anwendung nicht unbedingt erforderlich und / oder sinnvoll ist), da Objekte aufgrund des Sprachentwurfs über eine Referenzsemantik verfügen. Diese Sprachen verfügen in der Regel über einen Speicherbereinigungsmechanismus, der auf der Erreichbarkeit und nicht auf dem Besitz des Bereichs oder der Referenzzählung basiert.

Wenn eine Referenz oder ein Zeiger (einschließlich einer konstanten Referenz) in C ++ (oder C) übergeben wird, wird dem Programmierer versichert, dass kein spezieller Code (benutzerdefinierte oder vom Compiler generierte Funktionen) ausgeführt wird, außer der Weitergabe des Adresswerts (Referenz oder Zeiger). Dies ist eine Klarheit des Verhaltens, mit der sich C ++ - Programmierer wohl fühlen.

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

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 zur anderen) zugute, da weniger kopiert und tief geklont werden muss. In anderen Sprachen ist es jedoch unlogisch, von einer solchen Bewegung von Objekten zu sprechen.


(Off-Topic-Bereich) Ein Bereich, der dem Artikel "Want Speed? Pass by Value!" Gewidmet ist. geschrieben in circa 2009.

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

Sub-TL; DR Das Design der r-Wert-Semantik in C ++ ermöglicht eine überraschend elegante benutzerseitige Semantik auf aSort Funktion. Dieses elegante ist unmöglich in anderen Sprachen zu modellieren (nachzuahmen).

Eine Sortierfunktion wird auf eine gesamte Datenstruktur angewendet. Wie oben erwähnt, ist es langsam, wenn viel kopiert wird. Als eine (praktisch relevante) Leistungsoptimierung 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, bei der die Eingabe nicht geändert wird. (Die Vorlage wurde 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 beim Sortieren einen ausgewogenen Ansatz für binäre Suchbäume anwenden würden, anstatt bei Arrays einen destruktiven Sortieralgorithmus anzuwenden. Daher ist die praktische Relevanz dieser Technik nicht so hoch, wie es scheint.


Wie sich die Compiler-Optimierung auf diese Antwort auswirkt

Wenn Inlining (und auch die Ganzprogrammoptimierung / Verbindungszeitoptimierung) auf mehreren Ebenen von Funktionsaufrufen angewendet wird, kann der Compiler den Datenfluss (manchmal erschöpfend) anzeigen. In diesem Fall kann der Compiler viele Optimierungen anwenden, von denen einige die Erstellung ganzer Objekte im Speicher verhindern. In der Regel spielt es in dieser Situation 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 sich nicht analysieren lässt (z. B. etwas in einer anderen Bibliothek außerhalb der Kompilierung oder ein Aufrufdiagramm, das einfach zu kompliziert ist), 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 memcpyFunktion kopiert werden . Auf einigen Plattformen generiert der Compiler SIMD-Anweisungen, um zwischen zwei Speicherorten zu wechseln, wobei jede Anweisung mehrere zehn Bytes (16 oder 32) bewegt.


Diskussion zum Thema Ausführlichkeit oder visuelle Unordnung

C ++ - Programmierer sind daran gewöhnt, dh solange ein Programmierer C ++ nicht hasst, ist der Aufwand für das Schreiben oder Lesen von const-Referenzen im Quellcode nicht schrecklich.

Möglicherweise wurden die Kosten-Nutzen-Analysen bereits mehrfach 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.

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

  • Ja, dies wirkt sich auf die Leistung der in dieser Sprache geschriebenen Software aus.
  • Wenn Compiler den Zweck von Code verstehen, ist er möglicherweise intelligent genug, um dies zu automatisieren
  • Leider würde der Compiler in Sprachen, die die Wandlungsfähigkeit (im Gegensatz zur funktionalen Reinheit) bevorzugen, die meisten Dinge als mutiert einstufen, daher würde die automatische Ableitung der Konstanz die meisten Dinge als nicht-konstant ablehnen
  • Der mentale Aufwand hängt von den Menschen ab; Leute, die dies für einen hohen mentalen Aufwand halten, hätten C ++ als brauchbare Programmiersprache abgelehnt.
rwong
quelle
Dies ist eine der Situationen , in denen ich wünsche ich zwei Antworten annehmen könnte , anstatt nur eine wählen, die ... seufzen
CharonX
8

In Donald Knuths Artikel "StructuredProgrammingWithGoToStatements" schrieb er: "Programmierer verschwenden enorme Zeit damit, über die Geschwindigkeit unkritischer Teile ihrer Programme nachzudenken oder sich Gedanken zu machen, und diese Effizienzversuche wirken sich tatsächlich stark negativ auf das Debugging und die Wartung aus Wir sollten kleine Wirkungsgrade vergessen, sagen wir in 97% der Fälle: Vorzeitige Optimierung ist die Wurzel allen Übels. - Vorzeitige Optimierung

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

Lawrence
quelle
3
"Wenn Sie nur eine auswählen müssen, wählen Sie Klarheit." Der zweite sollte vorgezogen werden, da Sie möglicherweise gezwungen sind, den anderen zu wählen.
Deduplizierer
@ Deduplicator Vielen Dank. Im Kontext des OP hat der Programmierer jedoch die Freiheit zu wählen.
Lawrence
Ihre Antwort lautet allerdings etwas allgemeiner ...
Deduplicator
@ Deduplicator Ah, aber der Kontext meiner Antwort ist (auch) der, den der Programmierer auswählt. Wenn die Wahl dem Programmierer aufgezwungen würde, wäre es nicht "du", der die Auswahl vornimmt :). Ich habe die von Ihnen vorgeschlagene Änderung in Betracht gezogen und möchte nicht, dass Sie meine Antwort entsprechend bearbeiten. Ich bevorzuge jedoch den vorhandenen Wortlaut, da er klarer ist.
Lawrence
7

Bei der Übergabe von ([const] [rvalue] reference) | (value) sollten die Absichten und Versprechungen 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
Richard Hodges
quelle
3

Theoretisch sollte die Antwort ja sein. Tatsächlich ist es in manchen Fällen sogar so, dass das Übergeben von Konstantenreferenzen anstelle eines Werts eine Pessimisierung darstellt, selbst wenn der übergebene Wert zu groß ist, um in einen einzigen Wert zu passen register (oder die meisten anderen Heuristiken, die verwendet werden, um zu bestimmen, wann der Wert überschritten werden soll oder nicht). Vor Jahren schrieb David Abrahams einen Artikel mit dem Titel "Want Speed? Pass by Value!" einige dieser Fälle abdecken. Es ist nicht mehr leicht zu finden, aber wenn Sie eine Kopie ausgraben können, ist es wert, gelesen zu werden (IMO).

Im konkreten Fall durch konstante Referenz zugeben, jedoch würde ich das Idiom sagen so gut etabliert , dass die Situation mehr oder weniger umgekehrt: es sei denn , Sie wissen , die Art wird char/ short/ int/ long, erwarten die Leute es von const weitergegeben zu sehen Verweis standardmäßig, so ist es wahrscheinlich am besten, dies zu tun, es sei denn, Sie haben einen ziemlich bestimmten Grund, etwas anderes zu tun.

Jerry Sarg
quelle